tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count, Lock
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  54from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  55from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  56from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  57
  58from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  59from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  60
  61import UniLogger as uLog  # Logger for TKSBrokerAPI
  62
  63
  64# --- Common technical parameters:
  65
  66PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  67uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  68uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  69uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  70
  71__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  72
  73CPU_COUNT = cpu_count()  # host's real CPU count
  74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  75
  76
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self.__lock = Lock()  # initialize multiprocessing mutex lock
 129
 130        self.aliases = TKS_TICKER_ALIASES
 131        """Some aliases instead official tickers.
 132
 133        See also: `TKSEnums.TKS_TICKER_ALIASES`
 134        """
 135
 136        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 137
 138        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 139
 140        self._ticker = ""
 141        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 142
 143        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 144        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 145
 146        See also: `SearchByTicker()`, `SearchInstruments()`.
 147        """
 148
 149        self._figi = ""
 150        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 151
 152        See also: `SearchByFIGI()`, `SearchInstruments()`.
 153        """
 154
 155        self.depth = 1
 156        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 157
 158        See also: `GetCurrentPrices()`.
 159        """
 160
 161        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 162        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 163
 164        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 165        """
 166
 167        uLogger.debug("Broker API server: {}".format(self.server))
 168
 169        self.timeout = 15
 170        """Server operations timeout in seconds. Default: `15`.
 171
 172        See also: `SendAPIRequest()`.
 173        """
 174
 175        self.headers = {
 176            "Content-Type": "application/json",
 177            "accept": "application/json",
 178            "Authorization": "Bearer {}".format(self.token),
 179            "x-app-name": "Tim55667757.TKSBrokerAPI",
 180        }
 181        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 182
 183        See also: `SendAPIRequest()`.
 184        """
 185
 186        self.body = None
 187        """Request body which send to broker server. Default: `None`.
 188
 189        See also: `SendAPIRequest()`.
 190        """
 191
 192        self.moreDebug = False
 193        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 194
 195        self.useHTMLReports = False
 196        """
 197        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 198        
 199        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 200        """
 201
 202        self.historyFile = None
 203        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 204
 205        See also: `History()`.
 206        """
 207
 208        self.htmlHistoryFile = "index.html"
 209        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 210
 211        See also: `ShowHistoryChart()`.
 212        """
 213
 214        self.instrumentsFile = "instruments.md"
 215        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 216
 217        See also: `ShowInstrumentsInfo()`.
 218        """
 219
 220        self.searchResultsFile = "search-results.md"
 221        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 222
 223        See also: `SearchInstruments()`.
 224        """
 225
 226        self.pricesFile = "prices.md"
 227        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 228
 229        See also: `GetListOfPrices()`.
 230        """
 231
 232        self.infoFile = "info.md"
 233        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 234
 235        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 236        """
 237
 238        self.bondsXLSXFile = "ext-bonds.xlsx"
 239        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 240        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 241
 242        See also: `ExtendBondsData()`.
 243        """
 244
 245        self.calendarFile = "calendar.md"
 246        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 247        
 248        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 249
 250        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 251        """
 252
 253        self.overviewFile = "overview.md"
 254        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 255
 256        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 257        """
 258
 259        self.overviewDigestFile = "overview-digest.md"
 260        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 261
 262        See also: `Overview()` with parameter `details="digest"`.
 263        """
 264
 265        self.overviewPositionsFile = "overview-positions.md"
 266        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 267
 268        See also: `Overview()` with parameter `details="positions"`.
 269        """
 270
 271        self.overviewOrdersFile = "overview-orders.md"
 272        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 273
 274        See also: `Overview()` with parameter `details="orders"`.
 275        """
 276
 277        self.overviewAnalyticsFile = "overview-analytics.md"
 278        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 279
 280        See also: `Overview()` with parameter `details="analytics"`.
 281        """
 282
 283        self.overviewBondsCalendarFile = "overview-calendar.md"
 284        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 285
 286        See also: `Overview()` with parameter `details="calendar"`.
 287        """
 288
 289        self.reportFile = "deals.md"
 290        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 291
 292        See also: `Deals()`.
 293        """
 294
 295        self.withdrawalLimitsFile = "limits.md"
 296        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 297
 298        See also: `OverviewLimits()` and `RequestLimits()`.
 299        """
 300
 301        self.userInfoFile = "user-info.md"
 302        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 303
 304        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 305        """
 306
 307        self.userAccountsFile = "accounts.md"
 308        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 309
 310        See also: `OverviewAccounts()`, `RequestAccounts()`.
 311        """
 312
 313        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 314        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 315
 316        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 317
 318        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 319        """
 320
 321        self.iList = None  # init iList for raw instruments data
 322        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 323        
 324        See also: `Listing()`, `DumpInstruments()`.
 325        """
 326
 327        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 328        if useCache:
 329            if os.path.exists(self.iListDumpFile):
 330                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 331                curTime = datetime.now(tzutc())
 332
 333                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 334                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 335
 336                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 337
 338                else:
 339                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 340
 341                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 342                        os.path.abspath(self.iListDumpFile),
 343                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 344                    ))
 345
 346            else:
 347                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 348                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 349
 350        else:
 351            self.iList = self.Listing()  # request new raw instruments data from broker server
 352            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 353
 354        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 355        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 356
 357        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 358        """
 359
 360    @property
 361    def ticker(self) -> str:
 362        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 363
 364        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 365        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 366
 367        See also: `SearchByTicker()`, `SearchInstruments()`.
 368        """
 369        return self._ticker
 370
 371    @ticker.setter
 372    def ticker(self, value):
 373        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 374
 375        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 376        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 377
 378        See also: `SearchByTicker()`, `SearchInstruments()`.
 379        """
 380        self._ticker = str(value).upper()  # Tickers may be upper case only
 381
 382    @property
 383    def figi(self) -> str:
 384        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 385
 386        See also: `SearchByFIGI()`, `SearchInstruments()`.
 387        """
 388        return self._figi
 389
 390    @figi.setter
 391    def figi(self, value):
 392        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 393
 394        See also: `SearchByFIGI()`, `SearchInstruments()`.
 395        """
 396        self._figi = str(value).upper()  # FIGI may be upper case only
 397
 398    def _ParseJSON(self, rawData="{}") -> dict:
 399        """
 400        Parse JSON from response string.
 401
 402        :param rawData: this is a string with JSON-formatted text.
 403        :return: JSON (dictionary), parsed from server response string.
 404        """
 405        responseJSON = json.loads(rawData) if rawData else {}
 406
 407        if self.moreDebug:
 408            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 409
 410        return responseJSON
 411
 412    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 413        """
 414        Send GET or POST request to broker server and receive JSON object.
 415
 416        self.header: must be defining with dictionary of headers.
 417        self.body: if define then used as request body. None by default.
 418        self.timeout: global request timeout, 15 seconds by default.
 419        :param url: url with REST request.
 420        :param reqType: send "GET" or "POST" request. "GET" by default.
 421        :param retry: how many times retry after first request if an 5xx server errors occurred.
 422        :param pause: sleep time in seconds between retries.
 423        :return: response JSON (dictionary) from broker.
 424        """
 425        if reqType.upper() not in ("GET", "POST"):
 426            uLogger.error("You can define request type: `GET` or `POST`!")
 427            raise Exception("Incorrect value")
 428
 429        if self.moreDebug:
 430            uLogger.debug("Request parameters:")
 431            uLogger.debug("    - REST API URL: {}".format(url))
 432            uLogger.debug("    - request type: {}".format(reqType))
 433            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 434            uLogger.debug("    - body:\n{}".format(self.body))
 435
 436        # fast hack to avoid all operations with some tickers/FIGI
 437        responseJSON = {}
 438        oK = True
 439        for item in self.exclude:
 440            if item in url:
 441                if self.moreDebug:
 442                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 443
 444                oK = False
 445                break
 446
 447        if oK:
 448            with self.__lock:  # acquire the mutex lock
 449                counter = 0
 450                response = None
 451                errMsg = ""
 452
 453                while not response and counter <= retry:
 454                    if reqType == "GET":
 455                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 456
 457                    if reqType == "POST":
 458                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 459
 460                    if self.moreDebug:
 461                        uLogger.debug("Response:")
 462                        uLogger.debug("    - status code: {}".format(response.status_code))
 463                        uLogger.debug("    - reason: {}".format(response.reason))
 464                        uLogger.debug("    - body length: {}".format(len(response.text)))
 465                        uLogger.debug("    - headers:\n{}".format(response.headers))
 466
 467                    # Server returns some headers:
 468                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 469                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 470                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 471                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 472                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 473                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 474                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 475                        sleep(rateLimitWait)
 476
 477                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 478                    if 400 <= response.status_code < 500:
 479                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 480                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 481
 482                        if "code" in response.text and "message" in response.text:
 483                            msgDict = self._ParseJSON(rawData=response.text)
 484                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 485
 486                        counter = retry + 1  # do not retry for 4xx errors
 487
 488                    if 500 <= response.status_code < 600:
 489                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 490                        uLogger.debug("    - not oK, {}".format(errMsg))
 491
 492                        if "code" in response.text and "message" in response.text:
 493                            errMsgDict = self._ParseJSON(rawData=response.text)
 494                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 495
 496                        counter += 1
 497
 498                        if counter <= retry:
 499                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 500                            sleep(pause)
 501
 502                responseJSON = self._ParseJSON(rawData=response.text)
 503
 504                if errMsg:
 505                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 506                    uLogger.error("    - not oK, {}".format(errMsg))
 507
 508        return responseJSON
 509
 510    def _IUpdater(self, iType: str) -> tuple:
 511        """
 512        Request instrument by type from server. See available API methods for instruments:
 513        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 514        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 515        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 516        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 517        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 518
 519        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 520        :return: tuple with iType name and list of available instruments of current type for defined user token.
 521        """
 522        result = []
 523
 524        if iType in TKS_INSTRUMENTS:
 525            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 526
 527            # all instruments have the same body in API v2 requests:
 528            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 529            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 530            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 531
 532        return iType, result
 533
 534    def _IWrapper(self, kwargs):
 535        """
 536        Wrapper runs instrument's update method `_IUpdater()`.
 537        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 538        """
 539        return self._IUpdater(**kwargs)
 540
 541    def Listing(self) -> dict:
 542        """
 543        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 544
 545        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 546        """
 547        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 548        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 549
 550        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 551        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 552        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 553
 554        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 555        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 556        poolUpdater.close()  # close the thread pool
 557        poolUpdater.join()  # wait a moment until all data returns from threads
 558
 559        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 560        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 561        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 562
 563        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 564        for iType in iList.keys():
 565            for ticker in iList[iType]:
 566                iList[iType][ticker]["type"] = iType
 567
 568                if "minPriceIncrement" in iList[iType][ticker].keys():
 569                    iList[iType][ticker]["step"] = NanoToFloat(
 570                        iList[iType][ticker]["minPriceIncrement"]["units"],
 571                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 572                    )
 573
 574                else:
 575                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 576
 577        return iList
 578
 579    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 580        """
 581        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 582
 583        See also: `DumpInstruments()`, `Listing()`.
 584
 585        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 586                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 587        """
 588        if self.iListDumpFile is None or not self.iListDumpFile:
 589            uLogger.error("Output name of dump file must be defined!")
 590            raise Exception("Filename required")
 591
 592        if not self.iList or forceUpdate:
 593            self.iList = self.Listing()
 594
 595        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 596
 597        # Save as XLSX with separated sheets for every type of instruments:
 598        with pd.ExcelWriter(
 599                path=xlsxDumpFile,
 600                date_format=TKS_DATE_FORMAT,
 601                datetime_format=TKS_DATE_TIME_FORMAT,
 602                mode="w",
 603        ) as writer:
 604            for iType in TKS_INSTRUMENTS:
 605                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 606                df = df[sorted(df)]  # sorted by column names
 607                df = df.applymap(
 608                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 609                    na_action="ignore",
 610                )  # converting numbers from nano-type to float in every cell
 611                df.to_excel(
 612                    writer,
 613                    sheet_name=iType,
 614                    encoding="UTF-8",
 615                    freeze_panes=(1, 1),
 616                )  # saving as XLSX-file with freeze first row and column as headers
 617
 618        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 619
 620    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 621        """
 622        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 623        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 624
 625        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 626
 627        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 628                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 629        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 630        """
 631        if self.iListDumpFile is None or not self.iListDumpFile:
 632            uLogger.error("Output name of dump file must be defined!")
 633            raise Exception("Filename required")
 634
 635        if not self.iList or forceUpdate:
 636            self.iList = self.Listing()
 637
 638        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 639        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 640            fH.write(jsonDump)
 641
 642        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 643
 644        return jsonDump
 645
 646    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 647        """
 648        Show information about one instrument defined by json data and prints it in Markdown format.
 649
 650        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 651
 652        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 653        :param show: if `True` then also printing information about instrument and its current price.
 654        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 655        :return: multilines text in Markdown format with information about one instrument.
 656        """
 657        splitLine = "|                                                             |                                                        |\n"
 658        infoText = ""
 659
 660        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 661            info = [
 662                "# Main information\n\n",
 663                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 664                "| Parameters                                                  | Values                                                 |\n",
 665                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 666                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 667                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 668            ]
 669
 670            if "sector" in iJSON.keys() and iJSON["sector"]:
 671                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 672
 673            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 674                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 675
 676            info.extend([
 677                splitLine,
 678                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 679                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 680            ])
 681
 682            if "isin" in iJSON.keys() and iJSON["isin"]:
 683                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 684
 685            if "classCode" in iJSON.keys():
 686                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 687
 688            info.extend([
 689                splitLine,
 690                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 691                splitLine,
 692                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 693                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 694                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 695            ])
 696
 697            if iJSON["figi"]:
 698                self._figi = iJSON["figi"]
 699                iJSON = iJSON | self.RequestTradingStatus()
 700
 701                info.extend([
 702                    splitLine,
 703                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 704                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 705                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 706                ])
 707
 708            info.append(splitLine)
 709
 710            if "type" in iJSON.keys() and iJSON["type"]:
 711                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 712
 713                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 714                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 715
 716            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 717                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 718
 719            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 720                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 721
 722            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 723                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 724
 725            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 726                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 727
 728            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 729                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 730
 731            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 732                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 733
 734            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 735                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 736
 737            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 738                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 739
 740            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 741                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 742
 743            if "currency" in iJSON.keys():
 744                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 745
 746            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 747                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 748
 749            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 750                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 751
 752            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 753                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 754
 755            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 756                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 757
 758            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 759                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 760
 761            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 762                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 763
 764            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 765                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 766
 767            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 768                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 769
 770            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 771                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 772
 773            iExt = None
 774            if iJSON["type"] == "Bonds":
 775                info.extend([
 776                    splitLine,
 777                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 778                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 779                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 780                        iJSON["nominal"]["currency"],
 781                    )),
 782                ])
 783
 784                if "floatingCouponFlag" in iJSON.keys():
 785                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 786
 787                if "amortizationFlag" in iJSON.keys():
 788                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 789
 790                info.append(splitLine)
 791
 792                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 793                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 794
 795                if iJSON["figi"]:
 796                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 797
 798                    info.extend([
 799                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 800                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 801                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 802                    ])
 803
 804                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 805                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 806                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 807                        iJSON["aciValue"]["currency"]
 808                    )))
 809
 810            if "currentPrice" in iJSON.keys():
 811                info.append(splitLine)
 812
 813                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 814                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 815
 816                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 817                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 818                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 819                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 820                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 821
 822                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 823                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 824
 825                info.extend([
 826                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 827                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 828                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 829                    )),
 830                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 831                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 832                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 833                    )),
 834                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 835                        "{:.2f}%{}".format(
 836                            iJSON["currentPrice"]["changes"],
 837                            " ({}{:.2f} {})".format(
 838                                "+" if bondChangesDelta > 0 else "",
 839                                bondChangesDelta,
 840                                aciCurrency
 841                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 842                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 843                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 844                                currency
 845                            ),
 846                        )
 847                    ),
 848                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 849                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 850                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 851                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 852                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 853                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 854                    )),
 855                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 856                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 858                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 859                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 860                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 861                    )),
 862                ])
 863
 864            if "lot" in iJSON.keys():
 865                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 866
 867            if "step" in iJSON.keys() and iJSON["step"] != 0:
 868                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 869
 870            # Add bond payment calendar:
 871            if iJSON["type"] == "Bonds":
 872                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 873                info.extend(["\n#", strCalendar])
 874
 875            infoText += "".join(info)
 876
 877            if show and not onlyFiles:
 878                uLogger.info("{}".format(infoText))
 879
 880            if self.infoFile is not None and (show or onlyFiles):
 881                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 882                    fH.write(infoText)
 883
 884                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 885
 886                if self.useHTMLReports:
 887                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 888                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 889                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 890
 891                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 892
 893        return infoText
 894
 895    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 896        """
 897        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 898
 899        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 900        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 901        :return: JSON formatted data with information about instrument.
 902        """
 903        tickerJSON = {}
 904        if self.moreDebug:
 905            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 906
 907        if not self._ticker:
 908            uLogger.warning("self._ticker variable is not be empty!")
 909
 910        else:
 911            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 912                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 913                raise Exception("Instrument not allowed")
 914
 915            if not self.iList:
 916                self.iList = self.Listing()
 917
 918            if self._ticker in self.iList["Shares"].keys():
 919                tickerJSON = self.iList["Shares"][self._ticker]
 920                if self.moreDebug:
 921                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 922
 923            elif self._ticker in self.iList["Currencies"].keys():
 924                tickerJSON = self.iList["Currencies"][self._ticker]
 925                if self.moreDebug:
 926                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 927
 928            elif self._ticker in self.iList["Bonds"].keys():
 929                tickerJSON = self.iList["Bonds"][self._ticker]
 930                if self.moreDebug:
 931                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 932
 933            elif self._ticker in self.iList["Etfs"].keys():
 934                tickerJSON = self.iList["Etfs"][self._ticker]
 935                if self.moreDebug:
 936                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 937
 938            elif self._ticker in self.iList["Futures"].keys():
 939                tickerJSON = self.iList["Futures"][self._ticker]
 940                if self.moreDebug:
 941                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 942
 943        if tickerJSON:
 944            self._figi = tickerJSON["figi"]
 945
 946            if requestPrice:
 947                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 948
 949                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 950                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 951
 952                else:
 953                    tickerJSON["currentPrice"]["changes"] = 0
 954
 955            if show:
 956                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 957
 958        else:
 959            if show:
 960                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 961
 962        return tickerJSON
 963
 964    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 965        """
 966        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 967
 968        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 969        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 970        :return: JSON formatted data with information about instrument.
 971        """
 972        figiJSON = {}
 973        if self.moreDebug:
 974            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 975
 976        if not self._figi:
 977            uLogger.warning("self._figi variable is not be empty!")
 978
 979        else:
 980            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 981                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 982                raise Exception("Instrument not allowed")
 983
 984            if not self.iList:
 985                self.iList = self.Listing()
 986
 987            for item in self.iList["Shares"].keys():
 988                if self._figi == self.iList["Shares"][item]["figi"]:
 989                    figiJSON = self.iList["Shares"][item]
 990
 991                    if self.moreDebug:
 992                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 993
 994                    break
 995
 996            if not figiJSON:
 997                for item in self.iList["Currencies"].keys():
 998                    if self._figi == self.iList["Currencies"][item]["figi"]:
 999                        figiJSON = self.iList["Currencies"][item]
1000
1001                        if self.moreDebug:
1002                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1003
1004                        break
1005
1006            if not figiJSON:
1007                for item in self.iList["Bonds"].keys():
1008                    if self._figi == self.iList["Bonds"][item]["figi"]:
1009                        figiJSON = self.iList["Bonds"][item]
1010
1011                        if self.moreDebug:
1012                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1013
1014                        break
1015
1016            if not figiJSON:
1017                for item in self.iList["Etfs"].keys():
1018                    if self._figi == self.iList["Etfs"][item]["figi"]:
1019                        figiJSON = self.iList["Etfs"][item]
1020
1021                        if self.moreDebug:
1022                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1023
1024                        break
1025
1026            if not figiJSON:
1027                for item in self.iList["Futures"].keys():
1028                    if self._figi == self.iList["Futures"][item]["figi"]:
1029                        figiJSON = self.iList["Futures"][item]
1030
1031                        if self.moreDebug:
1032                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1033
1034                        break
1035
1036        if figiJSON:
1037            self._figi = figiJSON["figi"]
1038            self._ticker = figiJSON["ticker"]
1039
1040            if requestPrice:
1041                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1042
1043                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1044                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1045
1046                else:
1047                    figiJSON["currentPrice"]["changes"] = 0
1048
1049            if show:
1050                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1051
1052        else:
1053            if show:
1054                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1055
1056        return figiJSON
1057
1058    def GetCurrentPrices(self, show: bool = True) -> dict:
1059        """
1060        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1061        `{"buy": [{"price": 1243.8, "quantity": 193},
1062                  {"price": 1244.0, "quantity": 168},
1063                  {"price": 1244.8, "quantity": 5},
1064                  {"price": 1245.0, "quantity": 61},
1065                  {"price": 1245.4, "quantity": 60}],
1066          "sell": [{"price": 1243.6, "quantity": 8},
1067                   {"price": 1242.6, "quantity": 10},
1068                   {"price": 1242.4, "quantity": 18},
1069                   {"price": 1242.2, "quantity": 50},
1070                   {"price": 1242.0, "quantity": 113}],
1071          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1072        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1073        - sell: list of dicts with Buyers prices,
1074            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1075            - quantity: volume value by current price in lots,
1076        - limitUp: current trade session limit price, maximum,
1077        - limitDown: current trade session limit price, minimum,
1078        - lastPrice: last deal price of the instrument,
1079        - closePrice: previous trade session close price of the instrument.
1080
1081        See also: `SearchByTicker()` and `SearchByFIGI()`.
1082        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1083        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1084
1085        :param show: if `True` then print DOM to log and console.
1086        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1087                 If an error occurred then returns an empty record:
1088                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1089        """
1090        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1091
1092        if self.depth < 1:
1093            uLogger.error("Depth of Market (DOM) must be >=1!")
1094            raise Exception("Incorrect value")
1095
1096        if not (self._ticker or self._figi):
1097            uLogger.error("self._ticker or self._figi variables must be defined!")
1098            raise Exception("Ticker or FIGI required")
1099
1100        if self._ticker and not self._figi:
1101            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1102            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1103
1104        if not self._ticker and self._figi:
1105            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1106            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1107
1108        if not self._figi:
1109            uLogger.error("FIGI is not defined!")
1110            raise Exception("Ticker or FIGI required")
1111
1112        else:
1113            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1114
1115            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1116            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1117            self.body = str({"figi": self._figi, "depth": self.depth})
1118            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1119
1120            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1121                # list of dicts with sellers orders:
1122                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1123
1124                # list of dicts with buyers orders:
1125                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1126
1127                # max price of instrument at this time:
1128                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1129
1130                # min price of instrument at this time:
1131                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1132
1133                # last price of deal with instrument:
1134                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1135
1136                # last close price of instrument:
1137                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1138
1139            else:
1140                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1141                uLogger.debug("Server response: {}".format(pricesResponse))
1142
1143            if show:
1144                if prices["buy"] or prices["sell"]:
1145                    info = [
1146                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1147                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1148                            self._ticker,
1149                            self._figi,
1150                            self.depth,
1151                        ),
1152                        "-" * 60, "\n",
1153                        "             Orders of Buyers | Orders of Sellers\n",
1154                        "-" * 60, "\n",
1155                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1156                        "-" * 60, "\n",
1157                    ]
1158
1159                    if not prices["buy"]:
1160                        info.append("                              | No orders!\n")
1161                        sumBuy = 0
1162
1163                    else:
1164                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1165                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1166                        for item in maxMinSorted:
1167                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1168
1169                    if not prices["sell"]:
1170                        info.append("No orders!                    |\n")
1171                        sumSell = 0
1172
1173                    else:
1174                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1175                        for item in prices["sell"]:
1176                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1177
1178                    info.extend([
1179                        "-" * 60, "\n",
1180                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1181                        "-" * 60, "\n",
1182                    ])
1183
1184                    infoText = "".join(info)
1185
1186                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1187
1188                else:
1189                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1190
1191        return prices
1192
1193    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1194        """
1195        This method get and show information about all available broker instruments for current user account.
1196        If `instrumentsFile` string is not empty then also save information to this file.
1197
1198        :param show: if `True` then print results to console, if `False` — print only to file.
1199        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1200        :return: multi-lines string with all available broker instruments.
1201        """
1202        if not self.iList:
1203            self.iList = self.Listing()
1204
1205        info = [
1206            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1207            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1208        ]
1209
1210        # add instruments count by type:
1211        for iType in self.iList.keys():
1212            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1213
1214        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1215        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1216
1217        # generating info tables with all instruments by type:
1218        for iType in self.iList.keys():
1219            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1220
1221            for instrument in self.iList[iType].keys():
1222                iName = self.iList[iType][instrument]["name"]  # instrument's name
1223                if len(iName) > 57:
1224                    iName = "{}...".format(iName[:54])  # right trim for a long string
1225
1226                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1227                    self.iList[iType][instrument]["ticker"],
1228                    iName,
1229                    self.iList[iType][instrument]["figi"],
1230                    self.iList[iType][instrument]["currency"],
1231                    self.iList[iType][instrument]["lot"],
1232                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1233                ))
1234
1235        infoText = "".join(info)
1236
1237        if show and not onlyFiles:
1238            uLogger.info(infoText)
1239
1240        if self.instrumentsFile and (show or onlyFiles):
1241            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1242                fH.write(infoText)
1243
1244            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1245
1246            if self.useHTMLReports:
1247                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1248                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1249                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1250
1251                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1252
1253        return infoText
1254
1255    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1256        """
1257        This method search and show information about instruments by part of its ticker, FIGI or name.
1258        If `searchResultsFile` string is not empty then also save information to this file.
1259
1260        :param pattern: string with part of ticker, FIGI or instrument's name.
1261        :param show: if `True` then print results to console, if `False` — return list of result only.
1262        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1263        :return: list of dictionaries with all found instruments.
1264        """
1265        if not self.iList:
1266            self.iList = self.Listing()
1267
1268        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1269        compiledPattern = re.compile(pattern, re.IGNORECASE)
1270
1271        for iType in self.iList:
1272            for instrument in self.iList[iType].values():
1273                searchResult = compiledPattern.search(" ".join(
1274                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1275                ))
1276
1277                if searchResult:
1278                    searchResults[iType][instrument["ticker"]] = instrument
1279
1280        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1281        info = [
1282            "# Search results\n\n",
1283            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1284            "* **Search pattern:** [{}]\n".format(pattern),
1285            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1286            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1287        ]
1288        infoShort = info[:]
1289
1290        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1291        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1292        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1293
1294        if resultsLen == 0:
1295            info.append("\nNo results\n")
1296            infoShort.append("\nNo results\n")
1297            uLogger.warning("No results. Try changing your search pattern.")
1298
1299        else:
1300            for iType in searchResults:
1301                iTypeValuesCount = len(searchResults[iType].values())
1302                if iTypeValuesCount > 0:
1303                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1304                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1305
1306                    for instrument in searchResults[iType].values():
1307                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1308                            instrument["type"],
1309                            instrument["ticker"],
1310                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1311                            instrument["figi"],
1312                        ))
1313
1314                    if iTypeValuesCount <= 5:
1315                        infoShort.extend(info[-iTypeValuesCount:])
1316
1317                    else:
1318                        infoShort.extend(info[-5:])
1319                        infoShort.append(skippedLine)
1320
1321        infoText = "".join(info)
1322        infoTextShort = "".join(infoShort)
1323
1324        if show and not onlyFiles:
1325            uLogger.info(infoTextShort)
1326            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1327
1328        if self.searchResultsFile and (show or onlyFiles):
1329            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1330                fH.write(infoText)
1331
1332            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1333
1334            if self.useHTMLReports:
1335                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1336                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1337                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1338
1339                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1340
1341        return searchResults
1342
1343    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1344        """
1345        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1346
1347        :param instruments: list of strings with tickers or FIGIs.
1348        :return: list with unique instrument FIGIs only.
1349        """
1350        requestedInstruments = []
1351        for iName in instruments:
1352            if iName not in self.aliases.keys():
1353                if iName not in requestedInstruments:
1354                    requestedInstruments.append(iName)
1355
1356            else:
1357                if iName not in requestedInstruments:
1358                    if self.aliases[iName] not in requestedInstruments:
1359                        requestedInstruments.append(self.aliases[iName])
1360
1361        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1362
1363        onlyUniqueFIGIs = []
1364        for iName in requestedInstruments:
1365            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1366                continue
1367
1368            self._ticker = iName
1369            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1370
1371            if not iData:
1372                self._ticker = ""
1373                self._figi = iName
1374
1375                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1376
1377                if not iData:
1378                    self._figi = ""
1379                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1380
1381            if iData and iData["figi"] not in onlyUniqueFIGIs:
1382                onlyUniqueFIGIs.append(iData["figi"])
1383
1384        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1385
1386        return onlyUniqueFIGIs
1387
1388    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1389        """
1390        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1391
1392        See limits: https://tinkoff.github.io/investAPI/limits/
1393
1394        If `pricesFile` string is not empty then also save information to this file.
1395
1396        :param instruments: list of strings with tickers or FIGIs.
1397        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1398        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1399        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1400                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1401        """
1402        if instruments is None or not instruments:
1403            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1404            raise Exception("Ticker or FIGI required")
1405
1406        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1407
1408        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1409
1410        iList = []  # trying to get info and current prices about all unique instruments:
1411        for self._figi in onlyUniqueFIGIs:
1412            iData = self.SearchByFIGI(requestPrice=True, show=False)
1413            iList.append(iData)
1414
1415        self.ShowListOfPrices(iList, show, onlyFiles)
1416
1417        return iList
1418
1419    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1420        """
1421        Show table contains current prices of given instruments.
1422
1423        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1424                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1425        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1426        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1427        :return: multilines text in Markdown format as a table contains current prices.
1428        """
1429        infoText = ""
1430
1431        if show or self.pricesFile or onlyFiles:
1432            info = [
1433                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1434                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1435                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1436            ]
1437
1438            for item in iList:
1439                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1440                    item["ticker"],
1441                    item["figi"],
1442                    item["type"],
1443                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1444                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1445                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1446                    "{} / {}".format(
1447                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1448                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1449                    ),
1450                    "{} / {}".format(
1451                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1452                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1453                    ),
1454                    item["currency"],
1455                ))
1456
1457            infoText = "".join(info)
1458
1459            if show and not onlyFiles:
1460                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1461
1462            if self.pricesFile and (show or onlyFiles):
1463                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1464                    fH.write(infoText)
1465
1466                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1467
1468                if self.useHTMLReports:
1469                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1470                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1471                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1472
1473                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1474
1475        return infoText
1476
1477    def RequestTradingStatus(self) -> dict:
1478        """
1479        Requesting trading status for the instrument defined by `figi` variable.
1480
1481        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1482
1483        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1484
1485        :return: dictionary with trading status attributes. Response example:
1486                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1487                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1488        """
1489        if self._figi is None or not self._figi:
1490            uLogger.error("Variable `figi` must be defined for using this method!")
1491            raise Exception("FIGI required")
1492
1493        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1494
1495        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1496        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1497        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1498
1499        if self.moreDebug:
1500            uLogger.debug("Records about current trading status successfully received")
1501
1502        return tradingStatus
1503
1504    def RequestPortfolio(self) -> dict:
1505        """
1506        Requesting actual user's portfolio for current `accountId`.
1507
1508        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1509
1510        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1511
1512        :return: dictionary with user's portfolio.
1513        """
1514        if self.accountId is None or not self.accountId:
1515            uLogger.error("Variable `accountId` must be defined for using this method!")
1516            raise Exception("Account ID required")
1517
1518        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1519
1520        self.body = str({"accountId": self.accountId})
1521        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1522        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1523
1524        if self.moreDebug:
1525            uLogger.debug("Records about user's portfolio successfully received")
1526
1527        return rawPortfolio
1528
1529    def RequestPositions(self) -> dict:
1530        """
1531        Requesting open positions by currencies and instruments for current `accountId`.
1532
1533        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1534
1535        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1536
1537        :return: dictionary with open positions by instruments.
1538        """
1539        if self.accountId is None or not self.accountId:
1540            uLogger.error("Variable `accountId` must be defined for using this method!")
1541            raise Exception("Account ID required")
1542
1543        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1544
1545        self.body = str({"accountId": self.accountId})
1546        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1547        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1548
1549        if self.moreDebug:
1550            uLogger.debug("Records about current open positions successfully received")
1551
1552        return rawPositions
1553
1554    def RequestPendingOrders(self) -> list:
1555        """
1556        Requesting current actual pending limit orders for current `accountId`.
1557
1558        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1559
1560        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1561
1562        :return: list of dictionaries with pending limit orders.
1563        """
1564        if self.accountId is None or not self.accountId:
1565            uLogger.error("Variable `accountId` must be defined for using this method!")
1566            raise Exception("Account ID required")
1567
1568        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1569
1570        self.body = str({"accountId": self.accountId})
1571        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1572        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1573
1574        if "orders" in rawResponse.keys():
1575            rawOrders = rawResponse["orders"]
1576            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1577
1578        else:
1579            rawOrders = []
1580            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1581
1582        return rawOrders
1583
1584    def RequestStopOrders(self) -> list:
1585        """
1586        Requesting current actual stop orders for current `accountId`.
1587
1588        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1589
1590        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1591
1592        :return: list of dictionaries with stop orders.
1593        """
1594        if self.accountId is None or not self.accountId:
1595            uLogger.error("Variable `accountId` must be defined for using this method!")
1596            raise Exception("Account ID required")
1597
1598        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1599
1600        self.body = str({"accountId": self.accountId})
1601        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1602        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1603
1604        if "stopOrders" in rawResponse.keys():
1605            rawStopOrders = rawResponse["stopOrders"]
1606            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1607
1608        else:
1609            rawStopOrders = []
1610            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1611
1612        return rawStopOrders
1613
1614    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1615        """
1616        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1617        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1618        and `overviewBondsCalendarFile` are defined then also save information to file.
1619
1620        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1621        many requests about the state of the portfolio, and then, based on the received data, a large number
1622        of calculation and statistics are collected.
1623
1624        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1625        :param details: how detailed should the information be?
1626        - `full` — shows full available information about portfolio status (by default),
1627        - `positions` — shows only open positions,
1628        - `orders` — shows only sections of open limits and stop orders.
1629        - `digest` — show a short digest of the portfolio status,
1630        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1631        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1632        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1633        :return: dictionary with client's raw portfolio and some statistics.
1634        """
1635        if self.accountId is None or not self.accountId:
1636            uLogger.error("Variable `accountId` must be defined for using this method!")
1637            raise Exception("Account ID required")
1638
1639        view = {
1640            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1641                "headers": {},  # list of dictionaries, response headers without "positions" section
1642                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1643                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1644                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1645                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1646                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1647                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1648                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1649                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1650                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1651            },
1652            "stat": {  # --- some statistics calculated using "raw" sections:
1653                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1654                "availableRUB": 0.,  # available rubles (without other currencies)
1655                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1656                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1657                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1658                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1659                "sharesCostRUB": 0.,  # costs of all shares in RUB
1660                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1661                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1662                "futuresCostRUB": 0.,  # costs of all futures in RUB
1663                "Currencies": [],  # list of dictionaries of all currencies statistics
1664                "Shares": [],  # list of dictionaries of all shares statistics
1665                "Bonds": [],  # list of dictionaries of all bonds statistics
1666                "Etfs": [],  # list of dictionaries of all etfs statistics
1667                "Futures": [],  # list of dictionaries of all futures statistics
1668                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1669                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1670                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1671                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1672                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1673            },
1674            "analytics": {  # --- some analytics of portfolio:
1675                "distrByAssets": {},  # portfolio distribution by assets
1676                "distrByCompanies": {},  # portfolio distribution by companies
1677                "distrBySectors": {},  # portfolio distribution by sectors
1678                "distrByCurrencies": {},  # portfolio distribution by currencies
1679                "distrByCountries": {},  # portfolio distribution by countries
1680                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1681            }
1682        }
1683
1684        details = details.lower()
1685        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1686        if details not in availableDetails:
1687            details = "full"
1688            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1689
1690        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1691
1692        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1693        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1694        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1695        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1696
1697        # save response headers without "positions" section:
1698        for key in portfolioResponse.keys():
1699            if key != "positions":
1700                view["raw"]["headers"][key] = portfolioResponse[key]
1701
1702            else:
1703                continue
1704
1705        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1706        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1707        for item in portfolioResponse["positions"]:
1708            if item["instrumentType"] == "currency":
1709                self._figi = item["figi"]
1710                if not self._figi and item["ticker"]:
1711                    self._ticker = item["ticker"]
1712                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1713
1714                curr = self.SearchByFIGI(requestPrice=False)
1715
1716                # current price of currency in RUB:
1717                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1718                    "name": curr["name"],
1719                    "currentPrice": NanoToFloat(
1720                        item["currentPrice"]["units"],
1721                        item["currentPrice"]["nano"]
1722                    ),
1723                }
1724
1725                view["raw"]["Currencies"].append(item)
1726
1727            elif item["instrumentType"] == "share":
1728                view["raw"]["Shares"].append(item)
1729
1730            elif item["instrumentType"] == "bond":
1731                view["raw"]["Bonds"].append(item)
1732
1733            elif item["instrumentType"] == "etf":
1734                view["raw"]["Etfs"].append(item)
1735
1736            elif item["instrumentType"] == "futures":
1737                view["raw"]["Futures"].append(item)
1738
1739            else:
1740                continue
1741
1742        # how many volume of currencies (by ISO currency name) are blocked:
1743        for item in view["raw"]["positions"]["blocked"]:
1744            blocked = NanoToFloat(item["units"], item["nano"])
1745            if blocked > 0:
1746                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1747
1748        # how many volume of instruments (by FIGI) are blocked:
1749        for item in view["raw"]["positions"]["securities"]:
1750            blocked = int(item["blocked"])
1751            if blocked > 0:
1752                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1753
1754        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1755
1756        if "rub" in allBlocked.keys():
1757            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1758
1759        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1760        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1761        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1762        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1763        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1764        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1765        view["stat"]["portfolioCostRUB"] = sum([
1766            view["stat"]["allCurrenciesCostRUB"],
1767            view["stat"]["sharesCostRUB"],
1768            view["stat"]["bondsCostRUB"],
1769            view["stat"]["etfsCostRUB"],
1770            view["stat"]["futuresCostRUB"],
1771        ])
1772
1773        # --- calculating some portfolio statistics:
1774        byComp = {}  # distribution by companies
1775        bySect = {}  # distribution by sectors
1776        byCurr = {}  # distribution by currencies (include RUB)
1777        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1778        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1779
1780        for item in portfolioResponse["positions"]:
1781            self._figi = item["figi"]
1782            if not self._figi and item["ticker"]:
1783                self._ticker = item["ticker"]
1784                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1785
1786            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1787
1788            if instrument:
1789                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1790                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1791
1792                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1793                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1794
1795                else:
1796                    blocked = 0
1797
1798                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1799                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1800                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1801                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1802                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1803                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1804                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1805                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1806                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1807                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1808                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1809                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1810
1811                statData = {
1812                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1813                    "ticker": instrument["ticker"],  # ticker by FIGI
1814                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1815                    "volume": volume,  # available volume of instrument
1816                    "lots": lots,  # volume in lots of instrument
1817                    "direction": direction,  # direction of an instrument's position: short or long
1818                    "blocked": blocked,  # blocked volume of currency or instrument
1819                    "currentPrice": curPrice,  # current instrument's price in basic asset
1820                    "average": average,  # current average position price
1821                    "cost": cost,  # current cost of all volume of instrument in basic asset
1822                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1823                    "costRUB": costRUB,  # cost of instrument in ruble
1824                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1825                    "profit": profit,  # expected profit at current moment
1826                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1827                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1828                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1829                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1830                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1831                    "step": instrument["step"],  # minimum price increment
1832                }
1833
1834                # adding distribution by unique countries:
1835                if statData["country"] not in byCountry.keys():
1836                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1837
1838                else:
1839                    byCountry[statData["country"]]["cost"] += costRUB
1840                    byCountry[statData["country"]]["percent"] += percentCostRUB
1841
1842                if item["instrumentType"] != "currency":
1843                    # adding distribution by unique companies:
1844                    if statData["name"]:
1845                        if statData["name"] not in byComp.keys():
1846                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1847
1848                        else:
1849                            byComp[statData["name"]]["cost"] += costRUB
1850                            byComp[statData["name"]]["percent"] += percentCostRUB
1851
1852                    # adding distribution by unique sectors:
1853                    if statData["sector"] not in bySect.keys():
1854                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1855
1856                    else:
1857                        bySect[statData["sector"]]["cost"] += costRUB
1858                        bySect[statData["sector"]]["percent"] += percentCostRUB
1859
1860                # adding distribution by unique currencies:
1861                if currency not in byCurr.keys():
1862                    byCurr[currency] = {
1863                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1864                        "cost": costRUB,
1865                        "percent": percentCostRUB
1866                    }
1867
1868                else:
1869                    byCurr[currency]["cost"] += costRUB
1870                    byCurr[currency]["percent"] += percentCostRUB
1871
1872                # saving statistics for every instrument:
1873                if item["instrumentType"] == "currency":
1874                    view["stat"]["Currencies"].append(statData)
1875
1876                    # update dict with free funds for trading (total - blocked) by currencies
1877                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1878                    view["stat"]["funds"][currency] = {
1879                        "total": volume,
1880                        "totalCostRUB": costRUB,  # total volume cost in rubles
1881                        "free": volume - blocked,
1882                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1883                    }
1884
1885                elif item["instrumentType"] == "share":
1886                    view["stat"]["Shares"].append(statData)
1887
1888                elif item["instrumentType"] == "bond":
1889                    view["stat"]["Bonds"].append(statData)
1890
1891                elif item["instrumentType"] == "etf":
1892                    view["stat"]["Etfs"].append(statData)
1893
1894                elif item["instrumentType"] == "Futures":
1895                    view["stat"]["Futures"].append(statData)
1896
1897                else:
1898                    continue
1899
1900        # total changes in Russian Ruble:
1901        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1902        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1903        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1904        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1905        view["stat"]["funds"]["rub"] = {
1906            "total": view["stat"]["availableRUB"],
1907            "totalCostRUB": view["stat"]["availableRUB"],
1908            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1909            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1910        }
1911
1912        # --- pending limit orders sector data:
1913        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1914        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1915
1916        for item in view["raw"]["orders"]:
1917            self._figi = item["figi"]
1918
1919            if item["figi"] not in uniquePendingOrdersFIGIs:
1920                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1921
1922                uniquePendingOrdersFIGIs.append(item["figi"])
1923                uniquePendingOrders[item["figi"]] = instrument
1924
1925            else:
1926                instrument = uniquePendingOrders[item["figi"]]
1927
1928            if instrument:
1929                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1930                orderType = TKS_ORDER_TYPES[item["orderType"]]
1931                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1932                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1933
1934                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1935                if item["direction"] == "ORDER_DIRECTION_BUY":
1936                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1937
1938                else:
1939                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1940
1941                # requested price for order execution:
1942                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1943
1944                # necessary changes in percent to reach target from current price:
1945                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1946
1947                view["stat"]["orders"].append({
1948                    "orderID": item["orderId"],  # orderId number parameter of current order
1949                    "figi": item["figi"],  # FIGI identification
1950                    "ticker": instrument["ticker"],  # ticker name by FIGI
1951                    "lotsRequested": item["lotsRequested"],  # requested lots value
1952                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1953                    "currentPrice": lastPrice,  # current instrument's price for defined action
1954                    "targetPrice": target,  # requested price for order execution in base currency
1955                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1956                    "percentChanges": changes,  # changes in percent to target from current price
1957                    "currency": item["currency"],  # instrument's currency name
1958                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1959                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1960                    "status": orderState,  # order status from TKS_ORDER_STATES
1961                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1962                })
1963
1964        # --- stop orders sector data:
1965        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1966        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1967
1968        for item in view["raw"]["stopOrders"]:
1969            self._figi = item["figi"]
1970
1971            if item["figi"] not in uniqueStopOrdersFIGIs:
1972                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1973
1974                uniqueStopOrdersFIGIs.append(item["figi"])
1975                uniqueStopOrders[item["figi"]] = instrument
1976
1977            else:
1978                instrument = uniqueStopOrders[item["figi"]]
1979
1980            if instrument:
1981                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1982                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1983                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1984
1985                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1986                if "expirationTime" in item.keys():
1987                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1988                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1989
1990                else:
1991                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1992                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1993
1994                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1995                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1996                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1997
1998                else:
1999                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2000
2001                # requested price when stop-order executed:
2002                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2003
2004                # price for limit-order, set up when stop-order executed:
2005                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2006
2007                # necessary changes in percent to reach target from current price:
2008                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2009
2010                view["stat"]["stopOrders"].append({
2011                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2012                    "figi": item["figi"],  # FIGI identification
2013                    "ticker": instrument["ticker"],  # ticker name by FIGI
2014                    "lotsRequested": item["lotsRequested"],  # requested lots value
2015                    "currentPrice": lastPrice,  # current instrument's price for defined action
2016                    "targetPrice": target,  # requested price for stop-order execution in base currency
2017                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2018                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2019                    "percentChanges": changes,  # changes in percent to target from current price
2020                    "currency": item["currency"],  # instrument's currency name
2021                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2022                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2023                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2024                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2025                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2026                })
2027
2028        # --- calculating data for analytics section:
2029        # portfolio distribution by assets:
2030        view["analytics"]["distrByAssets"] = {
2031            "Ruble": {
2032                "uniques": 1,
2033                "cost": view["stat"]["availableRUB"],
2034                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2035            },
2036            "Currencies": {
2037                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2038                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2039                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2040            },
2041            "Shares": {
2042                "uniques": len(view["stat"]["Shares"]),
2043                "cost": view["stat"]["sharesCostRUB"],
2044                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2045            },
2046            "Bonds": {
2047                "uniques": len(view["stat"]["Bonds"]),
2048                "cost": view["stat"]["bondsCostRUB"],
2049                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2050            },
2051            "Etfs": {
2052                "uniques": len(view["stat"]["Etfs"]),
2053                "cost": view["stat"]["etfsCostRUB"],
2054                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2055            },
2056            "Futures": {
2057                "uniques": len(view["stat"]["Futures"]),
2058                "cost": view["stat"]["futuresCostRUB"],
2059                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2060            },
2061        }
2062
2063        # portfolio distribution by companies:
2064        view["analytics"]["distrByCompanies"]["All money cash"] = {
2065            "ticker": "",
2066            "cost": view["stat"]["allCurrenciesCostRUB"],
2067            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2068        }
2069        view["analytics"]["distrByCompanies"].update(byComp)
2070
2071        # portfolio distribution by sectors:
2072        view["analytics"]["distrBySectors"]["All money cash"] = {
2073            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2074            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2075        }
2076        view["analytics"]["distrBySectors"].update(bySect)
2077
2078        # portfolio distribution by currencies:
2079        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2080            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2081
2082            if self.moreDebug:
2083                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2084
2085        view["analytics"]["distrByCurrencies"].update(byCurr)
2086        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2087        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2088
2089        # portfolio distribution by countries:
2090        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2091            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2092
2093            if self.moreDebug:
2094                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2095
2096        view["analytics"]["distrByCountries"].update(byCountry)
2097        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2098        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2099
2100        # --- Prepare text statistics overview in human-readable:
2101        if show or onlyFiles:
2102            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2103
2104            # Whatever the value `details`, header not changes:
2105            info = [
2106                "# Client's portfolio\n\n",
2107                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2108                "* **Account ID:** [{}]\n".format(self.accountId),
2109            ]
2110
2111            if details in ["full", "positions", "digest"]:
2112                info.extend([
2113                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2114                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2115                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2116                        view["stat"]["totalChangesRUB"],
2117                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2118                        view["stat"]["totalChangesPercentRUB"],
2119                    ),
2120                ])
2121
2122            if details in ["full", "positions"]:
2123                info.extend([
2124                    "## Open positions\n\n",
2125                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2126                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2127                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2128                        "{:.2f} ({:.2f}) rub".format(
2129                            view["stat"]["availableRUB"],
2130                            view["stat"]["blockedRUB"],
2131                        )
2132                    )
2133                ])
2134
2135                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2136                    return [
2137                        "|                             |                                 |          |              |              |                     |                              |\n",
2138                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2139                            noTradeStr if noTradeStr else typeStr,
2140                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2141                        ),
2142                    ]
2143
2144                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2145                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2146                        "{} [{}]".format(data["ticker"], data["figi"]),
2147                        "{:.2f} ({:.2f}) {}".format(
2148                            data["volume"],
2149                            data["blocked"],
2150                            data["currency"],
2151                        ) if isCurr else "{:.0f} ({:.0f})".format(
2152                            data["volume"],
2153                            data["blocked"],
2154                        ),
2155                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2156                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2157                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2158                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2159                        "{}{:.2f} {} ({}{:.2f}%)".format(
2160                            "+" if data["profit"] > 0 else "",
2161                            data["profit"], data["baseCurrencyName"],
2162                            "+" if data["percentProfit"] > 0 else "",
2163                            data["percentProfit"],
2164                        ),
2165                    )
2166
2167                # --- Show currencies section:
2168                if view["stat"]["Currencies"]:
2169                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2170                    for item in view["stat"]["Currencies"]:
2171                        info.append(_InfoStr(item, isCurr=True))
2172
2173                else:
2174                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2175
2176                # --- Show shares section:
2177                if view["stat"]["Shares"]:
2178                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2179
2180                    for item in view["stat"]["Shares"]:
2181                        info.append(_InfoStr(item))
2182
2183                else:
2184                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2185
2186                # --- Show bonds section:
2187                if view["stat"]["Bonds"]:
2188                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2189
2190                    for item in view["stat"]["Bonds"]:
2191                        info.append(_InfoStr(item))
2192
2193                else:
2194                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2195
2196                # --- Show etfs section:
2197                if view["stat"]["Etfs"]:
2198                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2199
2200                    for item in view["stat"]["Etfs"]:
2201                        info.append(_InfoStr(item))
2202
2203                else:
2204                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2205
2206                # --- Show futures section:
2207                if view["stat"]["Futures"]:
2208                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2209
2210                    for item in view["stat"]["Futures"]:
2211                        info.append(_InfoStr(item))
2212
2213                else:
2214                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2215
2216            if details in ["full", "orders"]:
2217                # --- Show pending limit orders section:
2218                if view["stat"]["orders"]:
2219                    info.extend([
2220                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2221                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2222                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2223                    ])
2224
2225                    for item in view["stat"]["orders"]:
2226                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2227                            "{} [{}]".format(item["ticker"], item["figi"]),
2228                            item["orderID"],
2229                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2230                            "{} {} ({}{:.2f}%)".format(
2231                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2232                                item["baseCurrencyName"],
2233                                "+" if item["percentChanges"] > 0 else "",
2234                                float(item["percentChanges"]),
2235                            ),
2236                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2237                            item["action"],
2238                            item["type"],
2239                            item["date"],
2240                        ))
2241
2242                else:
2243                    info.append("\n## Total pending limit-orders: [0]\n")
2244
2245                # --- Show stop orders section:
2246                if view["stat"]["stopOrders"]:
2247                    info.extend([
2248                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2249                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2250                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2251                    ])
2252
2253                    for item in view["stat"]["stopOrders"]:
2254                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2255                            "{} [{}]".format(item["ticker"], item["figi"]),
2256                            item["orderID"],
2257                            item["lotsRequested"],
2258                            "{} {} ({}{:.2f}%)".format(
2259                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2260                                item["baseCurrencyName"],
2261                                "+" if item["percentChanges"] > 0 else "",
2262                                float(item["percentChanges"]),
2263                            ),
2264                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2265                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2266                            item["action"],
2267                            item["type"],
2268                            item["expType"],
2269                            item["createDate"],
2270                            item["expDate"],
2271                        ))
2272
2273                else:
2274                    info.append("\n## Total stop-orders: [0]\n")
2275
2276            if details in ["full", "analytics"]:
2277                # -- Show analytics section:
2278                if view["stat"]["portfolioCostRUB"] > 0:
2279                    info.extend([
2280                        "\n# Analytics\n\n"
2281                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2282                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2283                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2284                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2285                            view["stat"]["totalChangesRUB"],
2286                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2287                            view["stat"]["totalChangesPercentRUB"],
2288                        ),
2289                        "\n## Portfolio distribution by assets\n"
2290                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2291                        "|------------------------------------|---------|---------|--------------------|\n",
2292                    ])
2293
2294                    for key in view["analytics"]["distrByAssets"].keys():
2295                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2296                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2297                                key,
2298                                view["analytics"]["distrByAssets"][key]["uniques"],
2299                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2300                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2301                            ))
2302
2303                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2304
2305                    info.extend([
2306                        "\n## Portfolio distribution by companies\n"
2307                        "\n| Company                                      | Percent | Current cost       |\n",
2308                        aSepLine,
2309                    ])
2310
2311                    for company in view["analytics"]["distrByCompanies"].keys():
2312                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2313                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2314                                "{}{}".format(
2315                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2316                                    company,
2317                                ),
2318                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2319                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2320                            ))
2321
2322                    info.extend([
2323                        "\n## Portfolio distribution by sectors\n"
2324                        "\n| Sector                                       | Percent | Current cost       |\n",
2325                        aSepLine,
2326                    ])
2327
2328                    for sector in view["analytics"]["distrBySectors"].keys():
2329                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2330                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2331                                sector,
2332                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2333                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2334                            ))
2335
2336                    info.extend([
2337                        "\n## Portfolio distribution by currencies\n"
2338                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2339                        aSepLine,
2340                    ])
2341
2342                    for curr in view["analytics"]["distrByCurrencies"].keys():
2343                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2344                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2345                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2346                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2347                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2348                            ))
2349
2350                    info.extend([
2351                        "\n## Portfolio distribution by countries\n"
2352                        "\n| Assets by country                            | Percent | Current cost       |\n",
2353                        aSepLine,
2354                    ])
2355
2356                    for country in view["analytics"]["distrByCountries"].keys():
2357                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2358                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2359                                country,
2360                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2361                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2362                            ))
2363
2364            if details in ["full", "calendar"]:
2365                # -- Show bonds payment calendar section:
2366                if view["stat"]["Bonds"]:
2367                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2368                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2369                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2370
2371                else:
2372                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2373
2374            infoText = "".join(info)
2375
2376            if show and not onlyFiles:
2377                uLogger.info(infoText)
2378
2379            if details == "full" and self.overviewFile:
2380                filename = self.overviewFile
2381
2382            elif details == "digest" and self.overviewDigestFile:
2383                filename = self.overviewDigestFile
2384
2385            elif details == "positions" and self.overviewPositionsFile:
2386                filename = self.overviewPositionsFile
2387
2388            elif details == "orders" and self.overviewOrdersFile:
2389                filename = self.overviewOrdersFile
2390
2391            elif details == "analytics" and self.overviewAnalyticsFile:
2392                filename = self.overviewAnalyticsFile
2393
2394            elif details == "calendar" and self.overviewBondsCalendarFile:
2395                filename = self.overviewBondsCalendarFile
2396
2397            else:
2398                filename = ""
2399
2400            if filename and (show or onlyFiles):
2401                with open(filename, "w", encoding="UTF-8") as fH:
2402                    fH.write(infoText)
2403
2404                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2405
2406                if self.useHTMLReports:
2407                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2408                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2409                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2410
2411                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2412
2413        return view
2414
2415    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2416        """
2417        Returns history operations between two given dates for current `accountId`.
2418        If `reportFile` string is not empty then also save human-readable report.
2419        Shows some statistical data of closed positions.
2420
2421        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2422        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2423        :param show: if `True` then also prints all records to the console.
2424        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2425        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2426        :return: original list of dictionaries with history of deals records from API ("operations" key):
2427                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2428                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2429        """
2430        if self.accountId is None or not self.accountId:
2431            uLogger.error("Variable `accountId` must be defined for using this method!")
2432            raise Exception("Account ID required")
2433
2434        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2435
2436        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2437
2438        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2439        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2440        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2441        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2442        customStat = {}  # custom statistics in additional to responseJSON
2443
2444        # --- output report in human-readable format:
2445        if show or onlyFiles or self.reportFile:
2446            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2447            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2448            nextDay = ""
2449
2450            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2451
2452            if len(ops) > 0:
2453                customStat = {
2454                    "opsCount": 0,  # total operations count
2455                    "buyCount": 0,  # buy operations
2456                    "sellCount": 0,  # sell operations
2457                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2458                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2459                    "payIn": {"rub": 0.},  # Deposit brokerage account
2460                    "payOut": {"rub": 0.},  # Withdrawals
2461                    "divs": {"rub": 0.},  # Dividends income
2462                    "coupons": {"rub": 0.},  # Coupon's income
2463                    "brokerCom": {"rub": 0.},  # Service commissions
2464                    "serviceCom": {"rub": 0.},  # Service commissions
2465                    "marginCom": {"rub": 0.},  # Margin commissions
2466                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2467                }
2468
2469                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2470                for item in ops:
2471                    if item["state"] == "OPERATION_STATE_EXECUTED":
2472                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2473
2474                        # count buy operations:
2475                        if "_BUY" in item["operationType"]:
2476                            customStat["buyCount"] += 1
2477
2478                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2479                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2480
2481                            else:
2482                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2483
2484                        # count sell operations:
2485                        elif "_SELL" in item["operationType"]:
2486                            customStat["sellCount"] += 1
2487
2488                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2489                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2490
2491                            else:
2492                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2493
2494                        # count incoming operations:
2495                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2496                            if item["payment"]["currency"] in customStat["payIn"].keys():
2497                                customStat["payIn"][item["payment"]["currency"]] += payment
2498
2499                            else:
2500                                customStat["payIn"][item["payment"]["currency"]] = payment
2501
2502                        # count withdrawals operations:
2503                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2504                            if item["payment"]["currency"] in customStat["payOut"].keys():
2505                                customStat["payOut"][item["payment"]["currency"]] += payment
2506
2507                            else:
2508                                customStat["payOut"][item["payment"]["currency"]] = payment
2509
2510                        # count dividends income:
2511                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2512                            if item["payment"]["currency"] in customStat["divs"].keys():
2513                                customStat["divs"][item["payment"]["currency"]] += payment
2514
2515                            else:
2516                                customStat["divs"][item["payment"]["currency"]] = payment
2517
2518                        # count coupon's income:
2519                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2520                            if item["payment"]["currency"] in customStat["coupons"].keys():
2521                                customStat["coupons"][item["payment"]["currency"]] += payment
2522
2523                            else:
2524                                customStat["coupons"][item["payment"]["currency"]] = payment
2525
2526                        # count broker commissions:
2527                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2528                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2529                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2530
2531                            else:
2532                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2533
2534                        # count service commissions:
2535                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2536                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2537                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2538
2539                            else:
2540                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2541
2542                        # count margin commissions:
2543                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2544                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2545                                customStat["marginCom"][item["payment"]["currency"]] += payment
2546
2547                            else:
2548                                customStat["marginCom"][item["payment"]["currency"]] = payment
2549
2550                        # count withholding taxes:
2551                        elif "_TAX" in item["operationType"]:
2552                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2553                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2554
2555                            else:
2556                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2557
2558                        else:
2559                            continue
2560
2561                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2562
2563                # --- view "Actions" lines:
2564                info.extend([
2565                    "| Report sections            |                               |                              |                      |                        |\n",
2566                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2567                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2568                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2569                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2570                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2571                    ),
2572                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2573                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2574                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2575                    ),
2576                ])
2577
2578                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2579                for key in opsKeys:
2580                    if key == "rub":
2581                        continue
2582
2583                    info.extend([
2584                        "|                            |                               | {:<28} |                      |                        |\n".format(
2585                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2586                        ),
2587                        "|                            |                               | {:<28} |                      |                        |\n".format(
2588                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2589                        ),
2590                    ])
2591
2592                info.append(splitLine1)
2593
2594                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2595                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2596                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2597                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2598                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2599                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2600                    )
2601
2602                # --- view "Payments" lines:
2603                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2604                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2605
2606                for key in paymentsKeys:
2607                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2608
2609                info.append(splitLine1)
2610
2611                # --- view "Commissions and taxes" lines:
2612                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2613                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2614
2615                for key in comKeys:
2616                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2617
2618                info.extend([
2619                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2620                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2621                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2622                ])
2623
2624            else:
2625                info.append("Broker returned no operations during this period\n")
2626
2627            # --- view "Operations" section:
2628            for item in ops:
2629                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2630                    continue
2631
2632                else:
2633                    self._figi = item["figi"]
2634                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2635                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2636
2637                    # group of deals during one day:
2638                    if nextDay and item["date"].split("T")[0] != nextDay:
2639                        info.append(splitLine2)
2640                        nextDay = ""
2641
2642                    else:
2643                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2644
2645                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2646                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2647                        self._figi if self._figi else "—",
2648                        instrument["ticker"] if instrument else "—",
2649                        instrument["type"] if instrument else "—",
2650                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2651                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2652                        TKS_OPERATION_STATES[item["state"]],
2653                        TKS_OPERATION_TYPES[item["operationType"]],
2654                    ))
2655
2656            infoText = "".join(info)
2657
2658            if show and not onlyFiles:
2659                if self.moreDebug:
2660                    uLogger.debug("Records about history of a client's operations successfully received")
2661
2662                uLogger.info(infoText)
2663
2664            if self.reportFile and (show or onlyFiles):
2665                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2666                    fH.write(infoText)
2667
2668                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2669
2670                if self.useHTMLReports:
2671                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2672                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2673                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2674
2675                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2676
2677        return ops, customStat
2678
2679    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2680        """
2681        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2682
2683        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2684        Warning! Broker server used ISO UTC time by default.
2685
2686        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2687        Also, `historyFile` used to update history with `onlyMissing` parameter.
2688
2689        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2690
2691        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2692        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2693        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2694                         `"hour"`, `"day"`. Default: `"hour"`.
2695        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2696                            False by default. Warning! History appends only from last candle to current time
2697                            with always update last candle!
2698        :param csvSep: separator if csv-file is used, `,` by default.
2699        :param show: if `True` then also prints Pandas DataFrame to the console.
2700        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2701        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2702                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2703        """
2704        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2705        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2706        history = None  # empty pandas object for history
2707
2708        if interval not in TKS_CANDLE_INTERVALS.keys():
2709            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2710            raise Exception("Incorrect value")
2711
2712        if not (self._ticker or self._figi):
2713            uLogger.error("Ticker or FIGI must be defined!")
2714            raise Exception("Ticker or FIGI required")
2715
2716        if self._ticker and not self._figi:
2717            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2718            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2719
2720        if self._figi and not self._ticker:
2721            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2722            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2723
2724        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2725        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2726        if interval.lower() != "day":
2727            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2728
2729        delta = dtEnd - dtStart  # current UTC time minus last time in file
2730        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2731
2732        # calculate history length in candles:
2733        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2734        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2735            length += 1  # to avoid fraction time
2736
2737        # calculate data blocks count:
2738        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2739
2740        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2741        if self.moreDebug:
2742            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2743            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2744            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2745            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2746
2747        tempOld = None  # pandas object for old history, if --only-missing key present
2748        lastTime = None  # datetime object of last old candle in file
2749
2750        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2751            if self.moreDebug:
2752                uLogger.debug("--only-missing key present, add only last missing candles...")
2753                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2754
2755            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2756
2757            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2758            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2759            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2760            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2761
2762            # get last datetime object from last string in file or minus 1 delta if file is empty:
2763            if len(tempOld) > 0:
2764                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2765
2766            else:
2767                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2768
2769            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2770
2771        responseJSONs = []  # raw history blocks of data
2772
2773        blockEnd = dtEnd
2774        for item in range(blocks):
2775            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2776            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2777
2778            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2779                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2780            ))
2781
2782            if blockStart == blockEnd:
2783                uLogger.debug("Skipped this zero-length block...")
2784
2785            else:
2786                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2787                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2788                self.body = str({
2789                    "figi": self._figi,
2790                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2791                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2792                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2793                })
2794                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2795
2796                if "code" in responseJSON.keys():
2797                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2798
2799                else:
2800                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2801                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2802
2803                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2804
2805            blockEnd = blockStart
2806
2807        printCount = len(responseJSONs)  # candles to show in console
2808        if responseJSONs:
2809            tempHistory = pd.DataFrame(
2810                data={
2811                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2812                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2813                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2814                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2815                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2816                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2817                    "volume": [int(item["volume"]) for item in responseJSONs],
2818                },
2819                index=range(len(responseJSONs)),
2820                columns=["date", "time", "open", "high", "low", "close", "volume"],
2821            )
2822            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2823            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2824
2825            # append only newest candles to old history if --only-missing key present:
2826            if onlyMissing and tempOld is not None and lastTime is not None:
2827                index = 0  # find start index in tempHistory data:
2828
2829                for i, item in tempHistory.iterrows():
2830                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2831
2832                    if curTime == lastTime:
2833                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2834                        index = i
2835                        printCount = index + 1
2836                        break
2837
2838                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2839
2840            else:
2841                history = tempHistory  # if no `--only-missing` key then load full data from server
2842
2843            if self.moreDebug:
2844                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2845
2846        if history is not None and not history.empty:
2847            if show and not onlyFiles:
2848                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2849                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2850                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2851                ))
2852
2853        else:
2854            uLogger.warning("Received an empty candles history!")
2855
2856        if self.historyFile is not None:
2857            if history is not None and not history.empty:
2858                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2859                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2860
2861            else:
2862                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2863
2864        else:
2865            if self.moreDebug:
2866                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2867
2868        return history
2869
2870    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2871        """
2872        Load candles history from csv-file and return Pandas DataFrame object.
2873
2874        See also: `History()` and `ShowHistoryChart()` methods.
2875
2876        :param filePath: path to csv-file to open.
2877        """
2878        loadedHistory = None  # init candles data object
2879
2880        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2881
2882        if os.path.exists(filePath):
2883            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2884
2885            tfStr = self.priceModel.FormattedDelta(
2886                self.priceModel.timeframe,
2887                "{days} days {hours}h {minutes}m {seconds}s",
2888            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2889                self.priceModel.timeframe,
2890                "{hours}h {minutes}m {seconds}s",
2891            )
2892
2893            if loadedHistory is not None and not loadedHistory.empty:
2894                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2895                    len(loadedHistory),
2896                    tfStr,
2897                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2898                )
2899
2900            else:
2901                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2902
2903        else:
2904            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2905
2906        return loadedHistory
2907
2908    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2909        """
2910        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2911
2912        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2913        Default: `index.html` (both for interact and non-interact candlesticks chart).
2914
2915        See also: `History()` and `LoadHistory()` methods.
2916
2917        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2918        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2919                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2920                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2921                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2922        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2923                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2924        """
2925        if isinstance(candles, str):
2926            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2927            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2928
2929        elif isinstance(candles, pd.DataFrame):
2930            self.priceModel.prices = candles  # set candles chain from variable
2931            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2932
2933            if "datetime" not in candles.columns:
2934                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2935
2936        else:
2937            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2938            raise Exception("Incorrect value")
2939
2940        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2941
2942        if interact:
2943            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2944
2945            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2946
2947        else:
2948            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2949
2950            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2951
2952        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2953
2954    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2955        """
2956        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2957        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2958
2959        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2960
2961        :param operation: string "Buy" or "Sell".
2962        :param lots: volume, integer count of lots >= 1.
2963        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2964        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2965        :param expDate: string "Undefined" by default or local date in future,
2966                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2967        :return: JSON with response from broker server.
2968        """
2969        if self.accountId is None or not self.accountId:
2970            uLogger.error("Variable `accountId` must be defined for using this method!")
2971            raise Exception("Account ID required")
2972
2973        if operation is None or not operation or operation not in ("Buy", "Sell"):
2974            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2975            raise Exception("Incorrect value")
2976
2977        if lots is None or lots < 1:
2978            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2979            lots = 1
2980
2981        if tp is None or tp < 0:
2982            tp = 0
2983
2984        if sl is None or sl < 0:
2985            sl = 0
2986
2987        if expDate is None or not expDate:
2988            expDate = "Undefined"
2989
2990        if not (self._ticker or self._figi):
2991            uLogger.error("Ticker or FIGI must be defined!")
2992            raise Exception("Ticker or FIGI required")
2993
2994        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2995        self._ticker = instrument["ticker"]
2996        self._figi = instrument["figi"]
2997
2998        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2999
3000        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3001        self.body = str({
3002            "figi": self._figi,
3003            "quantity": str(lots),
3004            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3005            "accountId": str(self.accountId),
3006            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3007        })
3008        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3009
3010        if "orderId" in response.keys():
3011            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3012                operation, response["orderId"],
3013                self._ticker, self._figi, lots,
3014                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3015                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3016                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3017            ))
3018
3019            if tp > 0:
3020                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3021
3022            if sl > 0:
3023                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3024
3025        else:
3026            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3027
3028        return response
3029
3030    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3031        """
3032        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3033        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3034
3035        See also: `Order()` and `Trade()` docstrings.
3036
3037        :param lots: volume, integer count of lots >= 1.
3038        :param tp: float > 0, take profit price of stop-order.
3039        :param sl: float > 0, stop loss price of stop-order.
3040        :param expDate: it's a local date in future.
3041                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3042        :return: JSON with response from broker server.
3043        """
3044        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3045
3046    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3047        """
3048        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3049        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3050
3051        See also: `Order()` and `Trade()` docstrings.
3052
3053        :param lots: volume, integer count of lots >= 1.
3054        :param tp: float > 0, take profit price of stop-order.
3055        :param sl: float > 0, stop loss price of stop-order.
3056        :param expDate: it's a local date in the future.
3057                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3058        :return: JSON with response from broker server.
3059        """
3060        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3061
3062    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3063        """
3064        Close position of given instruments.
3065
3066        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3067        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3068                         This avoids unnecessary downloading data from the server.
3069        """
3070        if instruments is None or not instruments:
3071            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3072            raise Exception("Ticker or FIGI required")
3073
3074        if isinstance(instruments, str):
3075            instruments = [instruments]
3076
3077        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3078        if uniqueInstruments:
3079            if portfolio is None or not portfolio:
3080                portfolio = self.Overview(show=False)
3081
3082            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3083            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3084
3085            for self._figi in uniqueInstruments:
3086                if self._figi not in allOpened:
3087                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3088                    continue
3089
3090                # search open trade info about instrument by ticker:
3091                instrument = {}
3092                for iType in TKS_INSTRUMENTS:
3093                    if instrument:
3094                        break
3095
3096                    for item in portfolio["stat"][iType]:
3097                        if item["figi"] == self._figi:
3098                            instrument = item
3099                            break
3100
3101                if instrument:
3102                    self._ticker = instrument["ticker"]
3103                    self._figi = instrument["figi"]
3104
3105                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3106                        self._ticker,
3107                        self._figi,
3108                        int(instrument["volume"]),
3109                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3110                    ))
3111
3112                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3113
3114                    if tradeLots > 0:
3115                        if instrument["blocked"] > 0:
3116                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3117                                instrument["blocked"],
3118                                self._ticker,
3119                                tradeLots,
3120                            ))
3121
3122                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3123                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3124
3125                    else:
3126                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3127
3128    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3129        """
3130        Close all positions of given instruments with defined type.
3131
3132        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3133        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3134                         This avoids unnecessary downloading data from the server.
3135        """
3136        if iType not in TKS_INSTRUMENTS:
3137            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3138
3139        else:
3140            if portfolio is None or not portfolio:
3141                portfolio = self.Overview(show=False)
3142
3143            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3144            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3145
3146            if tickers and portfolio:
3147                self.CloseTrades(tickers, portfolio)
3148
3149            else:
3150                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3151
3152    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3153        """
3154        Universal method to create market or limit orders with all available parameters for current `accountId`.
3155        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3156
3157        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3158        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3159
3160        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3161        then broker immediately open market order as you can do simple --buy or --sell operations!
3162
3163        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3164        When current price will go up or down to target price value then broker opens a limit order.
3165        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3166
3167        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3168
3169        :param operation: string "Buy" or "Sell".
3170        :param orderType: string "Limit" or "Stop".
3171        :param lots: volume, integer count of lots >= 1.
3172        :param targetPrice: target price > 0. This is open trade price for limit order.
3173        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3174                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3175        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3176                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3177                         Stop loss order always executed by market price.
3178        :param expDate: string "Undefined" by default or local date in future.
3179                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3180                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3181                        A limit order has no expiration date, it lasts until the end of the trading day.
3182        :return: JSON with response from broker server.
3183        """
3184        if self.accountId is None or not self.accountId:
3185            uLogger.error("Variable `accountId` must be defined for using this method!")
3186            raise Exception("Account ID required")
3187
3188        if operation is None or not operation or operation not in ("Buy", "Sell"):
3189            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3190            raise Exception("Incorrect value")
3191
3192        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3193            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3194            raise Exception("Incorrect value")
3195
3196        if lots is None or lots < 1:
3197            uLogger.error("You must define trade volume > 0: integer count of lots!")
3198            raise Exception("Incorrect value")
3199
3200        if targetPrice is None or targetPrice <= 0:
3201            uLogger.error("Target price for limit-order must be greater than 0!")
3202            raise Exception("Incorrect value")
3203
3204        if limitPrice is None or limitPrice <= 0:
3205            limitPrice = targetPrice
3206
3207        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3208            stopType = "Limit"
3209
3210        if expDate is None or not expDate:
3211            expDate = "Undefined"
3212
3213        if not (self._ticker or self._figi):
3214            uLogger.error("Tocker or FIGI must be defined!")
3215            raise Exception("Ticker or FIGI required")
3216
3217        response = {}
3218        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3219        self._ticker = instrument["ticker"]
3220        self._figi = instrument["figi"]
3221
3222        if orderType == "Limit":
3223            uLogger.debug(
3224                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3225                    self._ticker, self._figi,
3226                    operation, lots, targetPrice, instrument["currency"],
3227                ))
3228
3229            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3230            self.body = str({
3231                "figi": self._figi,
3232                "quantity": str(lots),
3233                "price": FloatToNano(targetPrice),
3234                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3235                "accountId": str(self.accountId),
3236                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3237            })
3238            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3239
3240            if "orderId" in response.keys():
3241                uLogger.info(
3242                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3243                        response["orderId"], self._ticker, self._figi, operation, lots,
3244                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3245                    ))
3246
3247                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3248                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3249                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3250                            targetPrice, instrument["currency"],
3251                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3252                        ))
3253
3254                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3255                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3256                            targetPrice, instrument["currency"],
3257                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3258                        ))
3259
3260            else:
3261                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3262
3263        if orderType == "Stop":
3264            uLogger.debug(
3265                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3266                    self._ticker, self._figi,
3267                    operation, lots,
3268                    targetPrice, instrument["currency"],
3269                    limitPrice, instrument["currency"],
3270                    stopType, expDate,
3271                ))
3272
3273            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3274            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3275            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3276
3277            body = {
3278                "figi": self._figi,
3279                "quantity": str(lots),
3280                "price": FloatToNano(limitPrice),
3281                "stopPrice": FloatToNano(targetPrice),
3282                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3283                "accountId": str(self.accountId),
3284                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3285                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3286            }
3287
3288            if expDateUTC:
3289                body["expireDate"] = expDateUTC
3290
3291            self.body = str(body)
3292            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3293
3294            if "stopOrderId" in response.keys():
3295                uLogger.info(
3296                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3297                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3298                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3299                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3300                        TKS_STOP_ORDER_TYPES[stopOrderType],
3301                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3302                    ))
3303
3304                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3305                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3306                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3307                            targetPrice, instrument["currency"],
3308                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3309                        ))
3310
3311                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3312                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3313                            targetPrice, instrument["currency"],
3314                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3315                        ))
3316
3317            else:
3318                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3319
3320        return response
3321
3322    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3323        """
3324        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3325        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3326        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3327        See also: `Order()` docstring.
3328
3329        :param lots: volume, integer count of lots >= 1.
3330        :param targetPrice: target price > 0. This is open trade price for limit order.
3331        :return: JSON with response from broker server.
3332        """
3333        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3334
3335    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3336        """
3337        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3338        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3339        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3340        target price value then broker opens a limit order. See also: `Order()` docstring.
3341
3342        :param lots: volume, integer count of lots >= 1.
3343        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3344        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3345                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3346        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3347                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3348        :param expDate: string "Undefined" by default or local date in future.
3349                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3350                        This date is converting to UTC format for server.
3351        :return: JSON with response from broker server.
3352        """
3353        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3354
3355    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3356        """
3357        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3358        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3359        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3360        See also: `Order()` docstring.
3361
3362        :param lots: volume, integer count of lots >= 1.
3363        :param targetPrice: target price > 0. This is open trade price for limit order.
3364        :return: JSON with response from broker server.
3365        """
3366        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3367
3368    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3369        """
3370        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3371        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3372        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3373        target price value then broker opens a limit order. See also: `Order()` docstring.
3374
3375        :param lots: volume, integer count of lots >= 1.
3376        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3377        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3378                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3379        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3380                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3381        :param expDate: string "Undefined" by default or local date in future.
3382                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3383                        This date is converting to UTC format for server.
3384        :return: JSON with response from broker server.
3385        """
3386        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3387
3388    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3389        """
3390        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3391
3392        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3393        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3394                             This avoids unnecessary downloading data from the server.
3395        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3396        """
3397        if self.accountId is None or not self.accountId:
3398            uLogger.error("Variable `accountId` must be defined for using this method!")
3399            raise Exception("Account ID required")
3400
3401        if orderIDs:
3402            if allOrdersIDs is None:
3403                rawOrders = self.RequestPendingOrders()
3404                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3405
3406            if allStopOrdersIDs is None:
3407                rawStopOrders = self.RequestStopOrders()
3408                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3409
3410            for orderID in orderIDs:
3411                idInPendingOrders = orderID in allOrdersIDs
3412                idInStopOrders = orderID in allStopOrdersIDs
3413
3414                if not (idInPendingOrders or idInStopOrders):
3415                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3416                    continue
3417
3418                else:
3419                    if idInPendingOrders:
3420                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3421
3422                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3423                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3424                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3425                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3426
3427                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3428                            if self.moreDebug:
3429                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3430
3431                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3432
3433                        else:
3434                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3435
3436                    elif idInStopOrders:
3437                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3438
3439                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3440                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3441                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3442                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3443
3444                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3445                            if self.moreDebug:
3446                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3447
3448                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3449
3450                        else:
3451                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3452
3453                    else:
3454                        continue
3455
3456    def CloseAllOrders(self) -> None:
3457        """
3458        Gets a list of open pending and stop orders and cancel it all.
3459        """
3460        rawOrders = self.RequestPendingOrders()
3461        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3462        lenOrders = len(allOrdersIDs)
3463
3464        rawStopOrders = self.RequestStopOrders()
3465        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3466        lenSOrders = len(allStopOrdersIDs)
3467
3468        if lenOrders > 0 or lenSOrders > 0:
3469            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3470
3471            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3472
3473        else:
3474            uLogger.info("Orders not found, nothing to cancel.")
3475
3476    def CloseAll(self, *args) -> None:
3477        """
3478        Close all available (not blocked) opened trades and orders.
3479
3480        Also, you can select one or more keywords case-insensitive:
3481        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3482
3483        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3484        """
3485        overview = self.Overview(show=False)  # get all open trades info
3486
3487        if len(args) == 0:
3488            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3489            self.CloseAllOrders()  # close all pending and stop orders
3490
3491            for iType in TKS_INSTRUMENTS:
3492                if iType != "Currencies":
3493                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3494
3495        else:
3496            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3497            lowerArgs = [x.lower() for x in args]
3498
3499            if "orders" in lowerArgs:
3500                self.CloseAllOrders()  # close all pending and stop orders
3501
3502            for iType in TKS_INSTRUMENTS:
3503                if iType.lower() in lowerArgs and iType != "Currencies":
3504                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3505
3506    def CloseAllByTicker(self, instrument: str) -> None:
3507        """
3508        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3509
3510        This method searches opened trade and orders of instrument throw all portfolio and then use
3511        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3512
3513        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3514
3515        :param instrument: string with ticker.
3516        """
3517        if instrument is None or not instrument:
3518            uLogger.error("Ticker name must be defined for using this method!")
3519            raise Exception("Ticker required")
3520
3521        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3522
3523        self._ticker = instrument  # try to set instrument as ticker
3524        self._figi = ""
3525
3526        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3527        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3528
3529        if limitAll and self.IsInLimitOrders(portfolio=overview):
3530            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3531            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3532
3533        if stopAll and self.IsInStopOrders(portfolio=overview):
3534            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3535            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3536
3537        if self.IsInPortfolio(portfolio=overview):
3538            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3539            self.CloseTrades(instruments=[instrument], portfolio=overview)
3540
3541    def CloseAllByFIGI(self, instrument: str) -> None:
3542        """
3543        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3544
3545        This method searches opened trade and orders of instrument throw all portfolio and then use
3546        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3547
3548        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3549
3550        :param instrument: string with FIGI id.
3551        """
3552        if instrument is None or not instrument:
3553            uLogger.error("FIGI id must be defined for using this method!")
3554            raise Exception("FIGI required")
3555
3556        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3557
3558        self._ticker = ""
3559        self._figi = instrument  # try to set instrument as FIGI id
3560
3561        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3562        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3563
3564        if limitAll and self.IsInLimitOrders(portfolio=overview):
3565            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3566            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3567
3568        if stopAll and self.IsInStopOrders(portfolio=overview):
3569            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3570            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3571
3572        if self.IsInPortfolio(portfolio=overview):
3573            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3574            self.CloseTrades(instruments=[instrument], portfolio=overview)
3575
3576    @staticmethod
3577    def ParseOrderParameters(operation, **inputParameters):
3578        """
3579        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3580
3581        :param operation: string "Buy" or "Sell".
3582        :param inputParameters: this is dict of strings that looks like this
3583               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3584               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3585               "prices" key: one or more prices to open limit-orders
3586               Counts of values in lots and prices lists must be equals!
3587        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3588        """
3589        # TODO: update order grid work with api v2
3590        pass
3591        # uLogger.debug("Input parameters: {}".format(inputParameters))
3592        #
3593        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3594        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3595        #     raise Exception("Incorrect value")
3596        #
3597        # if "l" in inputParameters.keys():
3598        #     inputParameters["lots"] = inputParameters.pop("l")
3599        #
3600        # if "p" in inputParameters.keys():
3601        #     inputParameters["prices"] = inputParameters.pop("p")
3602        #
3603        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3604        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3605        #     raise Exception("Incorrect value")
3606        #
3607        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3608        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3609        #
3610        # if len(lots) != len(prices):
3611        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3612        #     raise Exception("Incorrect value")
3613        #
3614        # uLogger.debug("Extracted parameters for orders:")
3615        # uLogger.debug("lots = {}".format(lots))
3616        # uLogger.debug("prices = {}".format(prices))
3617        #
3618        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3619        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3620        # uLogger.debug("Order parameters: {}".format(result))
3621        #
3622        # return result
3623
3624    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3625        """
3626        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3627
3628        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3629        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3630        """
3631        result = False
3632        msg = "Instrument not defined!"
3633
3634        if portfolio is None or not portfolio:
3635            portfolio = self.Overview(show=False)
3636
3637        if self._ticker:
3638            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3639            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3640
3641            for iType in TKS_INSTRUMENTS:
3642                for instrument in portfolio["stat"][iType]:
3643                    if instrument["ticker"] == self._ticker:
3644                        result = True
3645                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3646                        break
3647
3648        elif self._figi:
3649            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3650            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3651
3652            for iType in TKS_INSTRUMENTS:
3653                for instrument in portfolio["stat"][iType]:
3654                    if instrument["figi"] == self._figi:
3655                        result = True
3656                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3657                        break
3658
3659        else:
3660            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3661
3662        uLogger.debug(msg)
3663
3664        return result
3665
3666    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3667        """
3668        Returns instrument from the user's portfolio if it presents there.
3669        Instrument must be defined by `ticker` (highly priority) or `figi`.
3670
3671        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3672        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3673        """
3674        result = None
3675        msg = "Instrument not defined!"
3676
3677        if portfolio is None or not portfolio:
3678            portfolio = self.Overview(show=False)
3679
3680        if self._ticker:
3681            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3682            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3683
3684            for iType in TKS_INSTRUMENTS:
3685                for instrument in portfolio["stat"][iType]:
3686                    if instrument["ticker"] == self._ticker:
3687                        result = instrument
3688                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3689                        break
3690
3691        elif self._figi:
3692            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3693            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3694
3695            for iType in TKS_INSTRUMENTS:
3696                for instrument in portfolio["stat"][iType]:
3697                    if instrument["figi"] == self._figi:
3698                        result = instrument
3699                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3700                        break
3701
3702        else:
3703            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3704
3705        uLogger.debug(msg)
3706
3707        return result
3708
3709    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3710        """
3711        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3712
3713        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3714
3715        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3716        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3717        """
3718        result = False
3719        msg = "Instrument not defined!"
3720
3721        if portfolio is None or not portfolio:
3722            portfolio = self.Overview(show=False)
3723
3724        if self._ticker:
3725            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3726            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3727
3728            for instrument in portfolio["stat"]["orders"]:
3729                if instrument["ticker"] == self._ticker:
3730                    result = True
3731                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3732                    break
3733
3734        elif self._figi:
3735            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3736            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3737
3738            for instrument in portfolio["stat"]["orders"]:
3739                if instrument["figi"] == self._figi:
3740                    result = True
3741                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3742                    break
3743
3744        else:
3745            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3746
3747        uLogger.debug(msg)
3748
3749        return result
3750
3751    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3752        """
3753        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3754        Instrument must be defined by `ticker` (highly priority) or `figi`.
3755
3756        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3757
3758        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3759        :return: list with `orderID`s of limit orders.
3760        """
3761        result = []
3762        msg = "Instrument not defined!"
3763
3764        if portfolio is None or not portfolio:
3765            portfolio = self.Overview(show=False)
3766
3767        if self._ticker:
3768            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3769            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3770
3771            for instrument in portfolio["stat"]["orders"]:
3772                if instrument["ticker"] == self._ticker:
3773                    result.append(instrument["orderID"])
3774
3775            if result:
3776                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3777
3778        elif self._figi:
3779            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3780            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3781
3782            for instrument in portfolio["stat"]["orders"]:
3783                if instrument["figi"] == self._figi:
3784                    result.append(instrument["orderID"])
3785
3786            if result:
3787                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3788
3789        else:
3790            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3791
3792        uLogger.debug(msg)
3793
3794        return result
3795
3796    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3797        """
3798        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3799
3800        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3801
3802        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3803        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3804        """
3805        result = False
3806        msg = "Instrument not defined!"
3807
3808        if portfolio is None or not portfolio:
3809            portfolio = self.Overview(show=False)
3810
3811        if self._ticker:
3812            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3813            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3814
3815            for instrument in portfolio["stat"]["stopOrders"]:
3816                if instrument["ticker"] == self._ticker:
3817                    result = True
3818                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3819                    break
3820
3821        elif self._figi:
3822            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3823            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3824
3825            for instrument in portfolio["stat"]["stopOrders"]:
3826                if instrument["figi"] == self._figi:
3827                    result = True
3828                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3829                    break
3830
3831        else:
3832            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3833
3834        uLogger.debug(msg)
3835
3836        return result
3837
3838    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3839        """
3840        Returns list with all `orderID`s of opened stop orders for the instrument.
3841        Instrument must be defined by `ticker` (highly priority) or `figi`.
3842
3843        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3844
3845        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3846        :return: list with `orderID`s of stop orders.
3847        """
3848        result = []
3849        msg = "Instrument not defined!"
3850
3851        if portfolio is None or not portfolio:
3852            portfolio = self.Overview(show=False)
3853
3854        if self._ticker:
3855            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3856            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3857
3858            for instrument in portfolio["stat"]["stopOrders"]:
3859                if instrument["ticker"] == self._ticker:
3860                    result.append(instrument["orderID"])
3861
3862            if result:
3863                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3864
3865        elif self._figi:
3866            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3867            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3868
3869            for instrument in portfolio["stat"]["stopOrders"]:
3870                if instrument["figi"] == self._figi:
3871                    result.append(instrument["orderID"])
3872
3873            if result:
3874                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3875
3876        else:
3877            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3878
3879        uLogger.debug(msg)
3880
3881        return result
3882
3883    def RequestLimits(self) -> dict:
3884        """
3885        Method for obtaining the available funds for withdrawal for current `accountId`.
3886
3887        See also:
3888        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3889        - `OverviewLimits()` method
3890
3891        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3892                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3893                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3894                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3895        """
3896        if self.accountId is None or not self.accountId:
3897            uLogger.error("Variable `accountId` must be defined for using this method!")
3898            raise Exception("Account ID required")
3899
3900        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3901
3902        self.body = str({"accountId": self.accountId})
3903        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3904        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3905
3906        if self.moreDebug:
3907            uLogger.debug("Records about available funds for withdrawal successfully received")
3908
3909        return rawLimits
3910
3911    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3912        """
3913        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3914
3915        See also: `RequestLimits()`.
3916
3917        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3918        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3919        :return: dict with raw parsed data from server and some calculated statistics about it.
3920        """
3921        if self.accountId is None or not self.accountId:
3922            uLogger.error("Variable `accountId` must be defined for using this method!")
3923            raise Exception("Account ID required")
3924
3925        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3926
3927        view = {
3928            "rawLimits": rawLimits,
3929            "limits": {  # parsed data for every currency:
3930                "money": {  # this is an array of portfolio currency positions
3931                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3932                },
3933                "blocked": {  # this is an array of blocked currency
3934                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3935                },
3936                "blockedGuarantee": {  # this is locked money under collateral for futures
3937                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3938                },
3939            },
3940        }
3941
3942        # --- Prepare text table with limits in human-readable format:
3943        if show or onlyFiles:
3944            info = [
3945                "# Withdrawal limits\n\n",
3946                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3947                "* **Account ID:** [{}]\n".format(self.accountId),
3948            ]
3949
3950            if view["limits"]["money"]:
3951                info.extend([
3952                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3953                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3954                ])
3955
3956            else:
3957                info.append("\nNo withdrawal limits\n")
3958
3959            for curr in view["limits"]["money"].keys():
3960                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3961                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3962                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3963
3964                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3965                    "[{}]".format(curr),
3966                    "{:.2f}".format(view["limits"]["money"][curr]),
3967                    "{:.2f}".format(availableMoney),
3968                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3969                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3970                )
3971
3972                if curr == "rub":
3973                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3974
3975                else:
3976                    info.append(infoStr)
3977
3978            infoText = "".join(info)
3979
3980            if show and not onlyFiles:
3981                uLogger.info(infoText)
3982
3983            if self.withdrawalLimitsFile and (show or onlyFiles):
3984                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3985                    fH.write(infoText)
3986
3987                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3988
3989                if self.useHTMLReports:
3990                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3991                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3992                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3993
3994                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3995
3996        return view
3997
3998    def RequestAccounts(self) -> dict:
3999        """
4000        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4001
4002        See also:
4003        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4004        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4005        - `OverviewUserInfo()` method
4006
4007        :return: dict with raw data from server that contains accounts info. Example of dict:
4008                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4009                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4010                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4011                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4012        """
4013        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4014
4015        self.body = str({})
4016        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4017        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4018
4019        if self.moreDebug:
4020            uLogger.debug("Records about available accounts successfully received")
4021
4022        return rawAccounts
4023
4024    def RequestUserInfo(self) -> dict:
4025        """
4026        Method for requesting common user's information.
4027
4028        See also:
4029        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4030        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4031        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4032        - `OverviewUserInfo()` method
4033
4034        :return: dict with raw data from server that contains user's information. Example of dict:
4035                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4036                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4037        """
4038        uLogger.debug("Requesting common user's information. Wait, please...")
4039
4040        self.body = str({})
4041        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4042        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4043
4044        if self.moreDebug:
4045            uLogger.debug("Records about current user successfully received")
4046
4047        return rawUserInfo
4048
4049    def RequestMarginStatus(self, accountId: str = None) -> dict:
4050        """
4051        Method for requesting margin calculation for defined account ID.
4052
4053        See also:
4054        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4055        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4056        - `OverviewUserInfo()` method
4057
4058        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4059        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4060                 Example of responses:
4061                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4062                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4063                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4064                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4065                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4066                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4067        """
4068        if accountId is None or not accountId:
4069            if self.accountId is None or not self.accountId:
4070                uLogger.error("Variable `accountId` must be defined for using this method!")
4071                raise Exception("Account ID required")
4072
4073            else:
4074                accountId = self.accountId  # use `self.accountId` (main ID) by default
4075
4076        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4077
4078        self.body = str({"accountId": accountId})
4079        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4080        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4081
4082        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4083            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4084            rawMargin = {}
4085
4086        else:
4087            if self.moreDebug:
4088                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4089
4090        return rawMargin
4091
4092    def RequestTariffLimits(self) -> dict:
4093        """
4094        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4095
4096        See also:
4097        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4098        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4099        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4100        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4101        - `OverviewUserInfo()` method
4102
4103        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4104                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4105                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4106        """
4107        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4108
4109        self.body = str({})
4110        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4111        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4112
4113        if self.moreDebug:
4114            uLogger.debug("Records with limits of current tariff successfully received")
4115
4116        return rawTariffLimits
4117
4118    def RequestBondCoupons(self, iJSON: dict) -> dict:
4119        """
4120        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4121        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4122        All dates are in UTC timezone.
4123
4124        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4125        Documentation:
4126        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4127        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4128
4129        See also: `ExtendBondsData()`.
4130
4131        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4132                      If raw iJSON is not data of bond then server returns an error [400] with message:
4133                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4134        :return: dictionary with bond payment calendar. Response example
4135                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4136                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4137                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4138                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4139        """
4140        if iJSON["figi"] is None or not iJSON["figi"]:
4141            uLogger.error("FIGI must be defined for using this method!")
4142            raise Exception("FIGI required")
4143
4144        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4145        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4146
4147        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4148            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4149            self._figi,
4150            startDate,
4151            endDate,
4152        ))
4153
4154        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4155        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4156        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4157
4158        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4159            uLogger.warning("Instrument type is not bond!")
4160
4161        else:
4162            if self.moreDebug:
4163                uLogger.debug("Records about bond payment calendar successfully received")
4164
4165        return calendar
4166
4167    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4168        """
4169        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4170        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4171        coupon yields, current yields and some statistics etc.
4172
4173        WARNING! This is too long operation if a lot of bonds requested from broker server.
4174
4175        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4176
4177        :param instruments: list of strings with tickers or FIGIs.
4178        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4179                     for further used by data scientists or stock analytics.
4180        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4181                 In XLSX-file and Pandas DataFrame fields mean:
4182                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4183                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4184        """
4185        if instruments is None or not instruments:
4186            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4187            raise Exception("Ticker or FIGI required")
4188
4189        if isinstance(instruments, str):
4190            instruments = [instruments]
4191
4192        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4193
4194        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4195
4196        iCount = len(uniqueInstruments)
4197        tooLong = iCount >= 20
4198        if tooLong:
4199            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4200
4201        bonds = None
4202        for i, self._figi in enumerate(uniqueInstruments):
4203            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4204
4205            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4206                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4207                rawBond = self.SearchByFIGI(requestPrice=True)
4208
4209                # Widen raw data with UTC current time (iData["actualDateTime"]):
4210                actualDate = datetime.now(tzutc())
4211                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4212
4213                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4214                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4215
4216                # Replace some values with human-readable:
4217                iData["nominalCurrency"] = iData["nominal"]["currency"]
4218                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4219                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4220                iData["aciCurrency"] = iData["aciValue"]["currency"]
4221                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4222                iData["issueSize"] = int(iData["issueSize"])
4223                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4224                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4225                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4226                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4227                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4228                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4229                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4230                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4231                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4232                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4233
4234                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4235                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4236                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4237                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4238                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4239                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4240                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4241                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4242                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4243                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4244                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4245
4246                # Widen raw data with calendar data from `rawCalendar` values:
4247                calendarData = []
4248                if "events" in iData["rawCalendar"].keys():
4249                    for item in iData["rawCalendar"]["events"]:
4250                        calendarData.append({
4251                            "couponDate": item["couponDate"],
4252                            "couponNumber": int(item["couponNumber"]),
4253                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4254                            "payCurrency": item["payOneBond"]["currency"],
4255                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4256                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4257                            "couponStartDate": item["couponStartDate"],
4258                            "couponEndDate": item["couponEndDate"],
4259                            "couponPeriod": item["couponPeriod"],
4260                        })
4261
4262                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4263                    if "maturityDate" not in iData.keys():
4264                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4265
4266                # Widen raw data with Coupon Rate.
4267                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4268                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4269                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4270                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4271
4272                # Widen raw data with Yield to Maturity (YTM) on current date.
4273                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4274                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4275                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4276                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4277                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4278                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4279
4280                iData["calendar"] = calendarData  # adds calendar at the end
4281
4282                # Remove not used data:
4283                iData.pop("uid")
4284                iData.pop("positionUid")
4285                iData.pop("currentPrice")
4286                iData.pop("rawCalendar")
4287
4288                colNames = list(iData.keys())
4289                if bonds is None:
4290                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4291
4292                else:
4293                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4294
4295            else:
4296                uLogger.warning("Instrument is not a bond!")
4297
4298            processed = round(100 * (i + 1) / iCount, 1)
4299            if tooLong and processed % 5 == 0:
4300                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4301
4302            else:
4303                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4304
4305        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4306
4307        # Saving bonds from Pandas DataFrame to XLSX sheet:
4308        if xlsx and self.bondsXLSXFile:
4309            with pd.ExcelWriter(
4310                    path=self.bondsXLSXFile,
4311                    date_format=TKS_DATE_FORMAT,
4312                    datetime_format=TKS_DATE_TIME_FORMAT,
4313                    mode="w",
4314            ) as writer:
4315                bonds.to_excel(
4316                    writer,
4317                    sheet_name="Extended bonds data",
4318                    index=True,
4319                    encoding="UTF-8",
4320                    freeze_panes=(1, 1),
4321                )  # saving as XLSX-file with freeze first row and column as headers
4322
4323            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4324
4325        return bonds
4326
4327    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4328        """
4329        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4330
4331        WARNING! This is too long operation if a lot of bonds requested from broker server.
4332
4333        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4334
4335        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4336                        extended information about bonds: main info, current prices, bond payment calendar,
4337                        coupon yields, current yields and some statistics etc.
4338                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4339        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4340                     for further used by data scientists or stock analytics.
4341        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4342        """
4343        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4344            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4345
4346        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4347
4348        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4349        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4350        calendar = None
4351        for bond in extBonds.iterrows():
4352            for item in bond[1]["calendar"]:
4353                cData = {
4354                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4355                    "couponDate": item["couponDate"],
4356                    "figi": bond[1]["figi"],
4357                    "ticker": bond[1]["ticker"],
4358                    "name": bond[1]["name"],
4359                    "couponNumber": item["couponNumber"],
4360                    "payOneBond": item["payOneBond"],
4361                    "payCurrency": item["payCurrency"],
4362                    "couponType": item["couponType"],
4363                    "couponPeriod": item["couponPeriod"],
4364                    "fixDate": item["fixDate"],
4365                    "couponStartDate": item["couponStartDate"],
4366                    "couponEndDate": item["couponEndDate"],
4367                }
4368
4369                if calendar is None:
4370                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4371
4372                else:
4373                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4374
4375        if calendar is not None:
4376            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4377
4378            # Saving calendar from Pandas DataFrame to XLSX sheet:
4379            if xlsx:
4380                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4381
4382                with pd.ExcelWriter(
4383                        path=xlsxCalendarFile,
4384                        date_format=TKS_DATE_FORMAT,
4385                        datetime_format=TKS_DATE_TIME_FORMAT,
4386                        mode="w",
4387                ) as writer:
4388                    humanReadable = calendar.copy(deep=True)
4389                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4390                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4391                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4392                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4393                    humanReadable.columns = colNames  # human-readable column names
4394
4395                    humanReadable.to_excel(
4396                        writer,
4397                        sheet_name="Bond payments calendar",
4398                        index=False,
4399                        encoding="UTF-8",
4400                        freeze_panes=(1, 2),
4401                    )  # saving as XLSX-file with freeze first row and column as headers
4402
4403                    del humanReadable  # release df in memory
4404
4405                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4406
4407        return calendar
4408
4409    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4410        """
4411        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4412        Also, creates Markdown file with calendar data, `calendar.md` by default.
4413
4414        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4415
4416        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4417                        extended information about bonds: main info, current prices, bond payment calendar,
4418                        coupon yields, current yields and some statistics etc.
4419                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4420        :param show: if `True` then also printing bonds payment calendar to the console,
4421                     otherwise save to file `calendarFile` only. `False` by default.
4422        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4423        :return: multilines text in Markdown format with bonds payment calendar as a table.
4424        """
4425        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4426            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4427
4428        infoText = "# Bond payments calendar\n\n"
4429
4430        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4431
4432        if not (calendar is None or calendar.empty):
4433            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4434
4435            info = [
4436                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4437                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4438                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4439            ]
4440
4441            newMonth = False
4442            notOneBond = calendar["figi"].nunique() > 1
4443            for i, bond in enumerate(calendar.iterrows()):
4444                if newMonth and notOneBond:
4445                    info.append(splitLine)
4446
4447                info.append(
4448                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4449                        "  √" if bond[1]["paid"] else "  —",
4450                        bond[1]["couponDate"].split("T")[0],
4451                        bond[1]["figi"],
4452                        bond[1]["ticker"],
4453                        bond[1]["couponNumber"],
4454                        "{} {}".format(
4455                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4456                            bond[1]["payCurrency"],
4457                        ),
4458                        bond[1]["couponType"],
4459                        bond[1]["couponPeriod"],
4460                        bond[1]["fixDate"].split("T")[0],
4461                    )
4462                )
4463
4464                if i < len(calendar.values) - 1:
4465                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4466                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4467                    newMonth = False if curDate.month == nextDate.month else True
4468
4469                else:
4470                    newMonth = False
4471
4472            infoText += "".join(info)
4473
4474            if show and not onlyFiles:
4475                uLogger.info("{}".format(infoText))
4476
4477            if self.calendarFile is not None and (show or onlyFiles):
4478                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4479                    fH.write(infoText)
4480
4481                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4482
4483                if self.useHTMLReports:
4484                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4485                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4486                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4487
4488                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4489
4490        else:
4491            infoText += "No data\n"
4492
4493        return infoText
4494
4495    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4496        """
4497        Method for parsing and show simple table with all available user accounts.
4498
4499        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4500
4501        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4502        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4503        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4504                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4505                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4506                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4507                                                        "closed": "—", "access": "Full access" }, ...}}`
4508        """
4509        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4510
4511        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4512        accounts = {
4513            item["id"]: {
4514                "type": TKS_ACCOUNT_TYPES[item["type"]],
4515                "name": item["name"],
4516                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4517                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4518                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4519                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4520            } for item in rawAccounts["accounts"]
4521        }
4522
4523        # Raw and parsed data with some fields replaced in "stat" section:
4524        view = {
4525            "rawAccounts": rawAccounts,
4526            "stat": accounts,
4527        }
4528
4529        # --- Prepare simple text table with only accounts data in human-readable format:
4530        if show or onlyFiles:
4531            info = [
4532                "# User accounts\n\n",
4533                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4534                "| Account ID   | Type                      | Status                    | Name                           |\n",
4535                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4536            ]
4537
4538            for account in view["stat"].keys():
4539                info.extend([
4540                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4541                        account,
4542                        view["stat"][account]["type"],
4543                        view["stat"][account]["status"],
4544                        view["stat"][account]["name"],
4545                    )
4546                ])
4547
4548            infoText = "".join(info)
4549
4550            if show and not onlyFiles:
4551                uLogger.info(infoText)
4552
4553            if self.userAccountsFile and (show or onlyFiles):
4554                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4555                    fH.write(infoText)
4556
4557                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4558
4559                if self.useHTMLReports:
4560                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4561                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4562                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4563
4564                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4565
4566        return view
4567
4568    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4569        """
4570        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4571
4572        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4573
4574        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4575        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4576        :return: dict with raw parsed data from server and some calculated statistics about it.
4577        """
4578        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4579        tmpTicker = self._ticker
4580        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4581        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4582        self._ticker = tmpTicker
4583
4584        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4585        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4586        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4587        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4588        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4589        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4590
4591        # This is dict with parsed common user data:
4592        userInfo = {
4593            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4594            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4595            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4596            "tariff": rawUserInfo["tariff"],
4597        }
4598
4599        # This is an array of dict with parsed margin statuses for every account IDs:
4600        margins = {}
4601        for accountId in accounts.keys():
4602            if rawMargins[accountId]:
4603                margins[accountId] = {
4604                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4605                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4606                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4607                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4608                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4609                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4610                    "missing": missing["volume"],
4611                }
4612
4613            else:
4614                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4615
4616        unary = {}  # unary-connection limits
4617        for item in rawTariffLimits["unaryLimits"]:
4618            if item["limitPerMinute"] in unary.keys():
4619                unary[item["limitPerMinute"]].extend(item["methods"])
4620
4621            else:
4622                unary[item["limitPerMinute"]] = item["methods"]
4623
4624        stream = {}  # stream-connection limits
4625        for item in rawTariffLimits["streamLimits"]:
4626            if item["limit"] in stream.keys():
4627                stream[item["limit"]].extend(item["streams"])
4628
4629            else:
4630                stream[item["limit"]] = item["streams"]
4631
4632        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4633        limits = {
4634            "unary": unary,
4635            "stream": stream,
4636        }
4637
4638        # Raw and parsed data as an output result:
4639        view = {
4640            "rawUserInfo": rawUserInfo,
4641            "rawAccounts": rawAccounts,
4642            "rawMargins": rawMargins,
4643            "rawTariffLimits": rawTariffLimits,
4644            "stat": {
4645                "overview": overview,
4646                "userInfo": userInfo,
4647                "accounts": accounts,
4648                "margins": margins,
4649                "limits": limits,
4650            },
4651        }
4652
4653        # --- Prepare text table with user information in human-readable format:
4654        if show or onlyFiles:
4655            info = [
4656                "# Full user information\n\n",
4657                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4658                "## Common information\n\n",
4659                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4660                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4661                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4662                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4663                "\n## User accounts\n\n",
4664            ]
4665
4666            for account in view["stat"]["accounts"].keys():
4667                info.extend([
4668                    "### ID: [{}]\n\n".format(account),
4669                    "| Parameters           | Values                                                       |\n",
4670                    "|----------------------|--------------------------------------------------------------|\n",
4671                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4672                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4673                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4674                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4675                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4676                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4677                ])
4678
4679                if margins[account]:
4680                    info.extend([
4681                        "| Margin status:       | Enabled                                                      |\n",
4682                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4683                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4684                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4685                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4686                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4687                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4688                    ])
4689
4690                else:
4691                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4692
4693            info.extend([
4694                "\n## Current user tariff limits\n",
4695                "\n### See also\n",
4696                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4697                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4698                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4699                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4700                "\n### Unary limits\n",
4701            ])
4702
4703            if unary:
4704                for key, values in sorted(unary.items()):
4705                    info.append("\n* Max requests per minute: {}\n".format(key))
4706
4707                    for value in values:
4708                        info.append("  - {}\n".format(value))
4709
4710            else:
4711                info.append("\nNot available\n")
4712
4713            info.append("\n### Stream limits\n")
4714
4715            if stream:
4716                for key, values in sorted(stream.items()):
4717                    info.append("\n* Max stream connections: {}\n".format(key))
4718
4719                    for value in values:
4720                        info.append("  - {}\n".format(value))
4721
4722            else:
4723                info.append("\nNot available\n")
4724
4725            infoText = "".join(info)
4726
4727            if show and not onlyFiles:
4728                uLogger.info(infoText)
4729
4730            if self.userInfoFile and (show or onlyFiles):
4731                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4732                    fH.write(infoText)
4733
4734                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4735
4736                if self.useHTMLReports:
4737                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4738                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4739                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4740
4741                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4742
4743        return view
4744
4745
4746class Args:
4747    """
4748    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4749    """
4750    def __init__(self, **kwargs):
4751        self.__dict__.update(kwargs)
4752
4753    def __getattr__(self, item):
4754        return None
4755
4756
4757def ParseArgs():
4758    """This function get and parse command line keys."""
4759    parser = ArgumentParser()  # command-line string parser
4760
4761    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4762    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4763
4764    # --- options:
4765
4766    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4767    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4768    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4769
4770    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4771    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4772
4773    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4774    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4775
4776    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4777    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4778
4779    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4780    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4781    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4782
4783    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4784    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4785
4786    # --- commands:
4787
4788    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4789
4790    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4791    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4792    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4793    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4794    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4795    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4796    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4797    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4798
4799    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4800    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4801    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4802    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4803    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4804    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4805
4806    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4807    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4808    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4809    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4810
4811    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4812    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4813    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4814
4815    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4816    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4817    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4818    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4819    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4820    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4821    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4822
4823    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4824    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4825    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4826    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4827    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4828
4829    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4830    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4831    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4832
4833    cmdArgs = parser.parse_args()
4834    return cmdArgs
4835
4836
4837def Main(**kwargs):
4838    """
4839    Main function for work with TKSBrokerAPI in the console.
4840
4841    See examples:
4842    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4843    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4844    """
4845    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4846
4847    if args.debug_level:
4848        uLogger.level = 10  # always debug level by default
4849        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4850
4851    exitCode = 0
4852    start = datetime.now(tzutc())
4853    uLogger.debug("=-" * 50)
4854    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4855        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4856        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4857    ))
4858
4859    # trying to calculate full current version:
4860    buildVersion = __version__
4861    try:
4862        v = version("tksbrokerapi")
4863        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4864
4865    except Exception:
4866        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4867
4868    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4869    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4870
4871    try:
4872        if args.version:
4873            print("TKSBrokerAPI {}".format(buildVersion))
4874            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4875
4876        else:
4877            # Init class for trading with Tinkoff Broker:
4878            trader = TinkoffBrokerServer(
4879                token=args.token,
4880                accountId=args.account_id,
4881                useCache=not args.no_cache,
4882            )
4883
4884            # --- set some options:
4885
4886            if args.more:
4887                trader.moreDebug = True
4888                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4889
4890            if args.html:
4891                trader.useHTMLReports = True
4892
4893            if args.ticker:
4894                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4895
4896                if ticker in trader.aliasesKeys:
4897                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4898
4899                else:
4900                    trader.ticker = ticker
4901
4902            if args.figi:
4903                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4904
4905            if args.depth is not None:
4906                trader.depth = args.depth
4907
4908            # --- do one command:
4909
4910            if args.list:
4911                if args.output is not None:
4912                    trader.instrumentsFile = args.output
4913
4914                trader.ShowInstrumentsInfo(show=True)
4915
4916            elif args.list_xlsx:
4917                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4918
4919            elif args.bonds_xlsx is not None:
4920                if args.output is not None:
4921                    trader.bondsXLSXFile = args.output
4922
4923                if len(args.bonds_xlsx) == 0:
4924                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4925
4926                else:
4927                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4928
4929            elif args.search:
4930                if args.output is not None:
4931                    trader.searchResultsFile = args.output
4932
4933                trader.SearchInstruments(pattern=args.search[0], show=True)
4934
4935            elif args.info:
4936                if not (args.ticker or args.figi):
4937                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4938                    raise Exception("Ticker or FIGI required")
4939
4940                if args.output is not None:
4941                    trader.infoFile = args.output
4942
4943                if args.ticker:
4944                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4945
4946                else:
4947                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4948
4949            elif args.calendar is not None:
4950                if args.output is not None:
4951                    trader.calendarFile = args.output
4952
4953                if len(args.calendar) == 0:
4954                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4955
4956                else:
4957                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4958
4959                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4960
4961            elif args.price:
4962                if not (args.ticker or args.figi):
4963                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4964                    raise Exception("Ticker or FIGI required")
4965
4966                trader.GetCurrentPrices(show=True)
4967
4968            elif args.prices is not None:
4969                if args.output is not None:
4970                    trader.pricesFile = args.output
4971
4972                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4973
4974            elif args.overview:
4975                if args.output is not None:
4976                    trader.overviewFile = args.output
4977
4978                trader.Overview(show=True, details="full")
4979
4980            elif args.overview_digest:
4981                if args.output is not None:
4982                    trader.overviewDigestFile = args.output
4983
4984                trader.Overview(show=True, details="digest")
4985
4986            elif args.overview_positions:
4987                if args.output is not None:
4988                    trader.overviewPositionsFile = args.output
4989
4990                trader.Overview(show=True, details="positions")
4991
4992            elif args.overview_orders:
4993                if args.output is not None:
4994                    trader.overviewOrdersFile = args.output
4995
4996                trader.Overview(show=True, details="orders")
4997
4998            elif args.overview_analytics:
4999                if args.output is not None:
5000                    trader.overviewAnalyticsFile = args.output
5001
5002                trader.Overview(show=True, details="analytics")
5003
5004            elif args.overview_calendar:
5005                if args.output is not None:
5006                    trader.overviewAnalyticsFile = args.output
5007
5008                trader.Overview(show=True, details="calendar")
5009
5010            elif args.deals is not None:
5011                if args.output is not None:
5012                    trader.reportFile = args.output
5013
5014                if 0 <= len(args.deals) < 3:
5015                    trader.Deals(
5016                        start=args.deals[0] if len(args.deals) >= 1 else None,
5017                        end=args.deals[1] if len(args.deals) == 2 else None,
5018                        show=True,  # Always show deals report in console
5019                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5020                    )
5021
5022                else:
5023                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5024                    raise Exception("Incorrect value")
5025
5026            elif args.history is not None:
5027                if args.output is not None:
5028                    trader.historyFile = args.output
5029
5030                if 0 <= len(args.history) < 3:
5031                    dataReceived = trader.History(
5032                        start=args.history[0] if len(args.history) >= 1 else None,
5033                        end=args.history[1] if len(args.history) == 2 else None,
5034                        interval="hour" if args.interval is None or not args.interval else args.interval,
5035                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5036                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5037                        show=True,  # shows all downloaded candles in console
5038                    )
5039
5040                    if args.render_chart is not None and dataReceived is not None:
5041                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5042
5043                        trader.ShowHistoryChart(
5044                            candles=dataReceived,
5045                            interact=iChart,
5046                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5047                        )
5048
5049                else:
5050                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5051                    raise Exception("Incorrect value")
5052
5053            elif args.load_history is not None:
5054                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5055
5056                if args.render_chart is not None and histData is not None:
5057                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5058                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5059
5060                    trader.ShowHistoryChart(
5061                        candles=histData,
5062                        interact=iChart,
5063                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5064                    )
5065
5066            elif args.trade is not None:
5067                if 1 <= len(args.trade) <= 5:
5068                    trader.Trade(
5069                        operation=args.trade[0],
5070                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5071                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5072                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5073                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5074                    )
5075
5076                else:
5077                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5078
5079            elif args.buy is not None:
5080                if 0 <= len(args.buy) <= 4:
5081                    trader.Buy(
5082                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5083                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5084                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5085                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5086                    )
5087
5088                else:
5089                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5090
5091            elif args.sell is not None:
5092                if 0 <= len(args.sell) <= 4:
5093                    trader.Sell(
5094                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5095                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5096                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5097                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5098                    )
5099
5100                else:
5101                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5102
5103            elif args.order:
5104                if 4 <= len(args.order) <= 7:
5105                    trader.Order(
5106                        operation=args.order[0],
5107                        orderType=args.order[1],
5108                        lots=int(args.order[2]),
5109                        targetPrice=float(args.order[3]),
5110                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5111                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5112                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5113                    )
5114
5115                else:
5116                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5117
5118            elif args.buy_limit:
5119                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5120
5121            elif args.sell_limit:
5122                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5123
5124            elif args.buy_stop:
5125                if 2 <= len(args.buy_stop) <= 7:
5126                    trader.BuyStop(
5127                        lots=int(args.buy_stop[0]),
5128                        targetPrice=float(args.buy_stop[1]),
5129                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5130                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5131                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5132                    )
5133
5134                else:
5135                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5136
5137            elif args.sell_stop:
5138                if 2 <= len(args.sell_stop) <= 7:
5139                    trader.SellStop(
5140                        lots=int(args.sell_stop[0]),
5141                        targetPrice=float(args.sell_stop[1]),
5142                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5143                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5144                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5145                    )
5146
5147                else:
5148                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5149
5150            # elif args.buy_order_grid is not None:
5151            #     # update order grid work with api v2
5152            #     if len(args.buy_order_grid) == 2:
5153            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5154            #
5155            #         for order in orderParams:
5156            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5157            #
5158            #     else:
5159            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5160            #
5161            # elif args.sell_order_grid is not None:
5162            #     # update order grid work with api v2
5163            #     if len(args.sell_order_grid) >= 2:
5164            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5165            #
5166            #         for order in orderParams:
5167            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5168            #
5169            #     else:
5170            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5171
5172            elif args.close_order is not None:
5173                trader.CloseOrders(args.close_order)  # close only one order
5174
5175            elif args.close_orders is not None:
5176                trader.CloseOrders(args.close_orders)  # close list of orders
5177
5178            elif args.close_trade:
5179                if not (args.ticker or args.figi):
5180                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5181                    raise Exception("Ticker or FIGI required")
5182
5183                if args.ticker:
5184                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5185
5186                else:
5187                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5188
5189            elif args.close_trades is not None:
5190                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5191
5192            elif args.close_all is not None:
5193                if args.ticker:
5194                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5195
5196                elif args.figi:
5197                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5198
5199                else:
5200                    trader.CloseAll(*args.close_all)
5201
5202            elif args.limits:
5203                if args.output is not None:
5204                    trader.withdrawalLimitsFile = args.output
5205
5206                trader.OverviewLimits(show=True)
5207
5208            elif args.user_info:
5209                if args.output is not None:
5210                    trader.userInfoFile = args.output
5211
5212                trader.OverviewUserInfo(show=True)
5213
5214            elif args.account:
5215                if args.output is not None:
5216                    trader.userAccountsFile = args.output
5217
5218                trader.OverviewAccounts(show=True)
5219
5220            else:
5221                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5222                raise Exception("There is no command to execute")
5223
5224    except Exception:
5225        trace = tb.format_exc()
5226        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5227            if e in trace:
5228                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5229                break
5230
5231        uLogger.debug(trace)
5232        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5233        exitCode = 255  # an error occurred, must be open a ticket for this issue
5234
5235    finally:
5236        finish = datetime.now(tzutc())
5237
5238        if exitCode == 0:
5239            if args.more:
5240                uLogger.debug("All operations were finished success (summary code is 0).")
5241
5242        else:
5243            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5244                os.path.abspath(uLog.defaultLogFile), exitCode,
5245            ))
5246
5247        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5248        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5249            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5250            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5251        ))
5252        uLogger.debug("=-" * 50)
5253
5254        if not kwargs:
5255            sys.exit(exitCode)
5256
5257        else:
5258            return exitCode
5259
5260
5261if __name__ == "__main__":
5262    Main()
class TinkoffBrokerServer:
  78class TinkoffBrokerServer:
  79    """
  80    This class implements methods to work with Tinkoff broker server.
  81
  82    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  83
  84    About `token`: https://tinkoff.github.io/investAPI/token/
  85    """
  86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  87        """
  88        Main class init.
  89
  90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  93        :param useCache: use default cache file with raw data to use instead of `iList`.
  94                         True by default. Cache is auto-update if new day has come.
  95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  96        :param defaultCache: path to default cache file. `dump.json` by default.
  97        """
  98        if token is None or not token:
  99            try:
 100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 102
 103            except KeyError:
 104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 105                raise Exception("Token required")
 106
 107        else:
 108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 110
 111        if accountId is None or not accountId:
 112            try:
 113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 115
 116            except KeyError:
 117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 118
 119        else:
 120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 122
 123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 125
 126        Latest version: https://pypi.org/project/tksbrokerapi/
 127        """
 128
 129        self.__lock = Lock()  # initialize multiprocessing mutex lock
 130
 131        self.aliases = TKS_TICKER_ALIASES
 132        """Some aliases instead official tickers.
 133
 134        See also: `TKSEnums.TKS_TICKER_ALIASES`
 135        """
 136
 137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 138
 139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 140
 141        self._ticker = ""
 142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 143
 144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 146
 147        See also: `SearchByTicker()`, `SearchInstruments()`.
 148        """
 149
 150        self._figi = ""
 151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 152
 153        See also: `SearchByFIGI()`, `SearchInstruments()`.
 154        """
 155
 156        self.depth = 1
 157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 158
 159        See also: `GetCurrentPrices()`.
 160        """
 161
 162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 164
 165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 166        """
 167
 168        uLogger.debug("Broker API server: {}".format(self.server))
 169
 170        self.timeout = 15
 171        """Server operations timeout in seconds. Default: `15`.
 172
 173        See also: `SendAPIRequest()`.
 174        """
 175
 176        self.headers = {
 177            "Content-Type": "application/json",
 178            "accept": "application/json",
 179            "Authorization": "Bearer {}".format(self.token),
 180            "x-app-name": "Tim55667757.TKSBrokerAPI",
 181        }
 182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 183
 184        See also: `SendAPIRequest()`.
 185        """
 186
 187        self.body = None
 188        """Request body which send to broker server. Default: `None`.
 189
 190        See also: `SendAPIRequest()`.
 191        """
 192
 193        self.moreDebug = False
 194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 195
 196        self.useHTMLReports = False
 197        """
 198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 199        
 200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 201        """
 202
 203        self.historyFile = None
 204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 205
 206        See also: `History()`.
 207        """
 208
 209        self.htmlHistoryFile = "index.html"
 210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 211
 212        See also: `ShowHistoryChart()`.
 213        """
 214
 215        self.instrumentsFile = "instruments.md"
 216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 217
 218        See also: `ShowInstrumentsInfo()`.
 219        """
 220
 221        self.searchResultsFile = "search-results.md"
 222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 223
 224        See also: `SearchInstruments()`.
 225        """
 226
 227        self.pricesFile = "prices.md"
 228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 229
 230        See also: `GetListOfPrices()`.
 231        """
 232
 233        self.infoFile = "info.md"
 234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 235
 236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 237        """
 238
 239        self.bondsXLSXFile = "ext-bonds.xlsx"
 240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 242
 243        See also: `ExtendBondsData()`.
 244        """
 245
 246        self.calendarFile = "calendar.md"
 247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 248        
 249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 250
 251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 252        """
 253
 254        self.overviewFile = "overview.md"
 255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 256
 257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 258        """
 259
 260        self.overviewDigestFile = "overview-digest.md"
 261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 262
 263        See also: `Overview()` with parameter `details="digest"`.
 264        """
 265
 266        self.overviewPositionsFile = "overview-positions.md"
 267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 268
 269        See also: `Overview()` with parameter `details="positions"`.
 270        """
 271
 272        self.overviewOrdersFile = "overview-orders.md"
 273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 274
 275        See also: `Overview()` with parameter `details="orders"`.
 276        """
 277
 278        self.overviewAnalyticsFile = "overview-analytics.md"
 279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 280
 281        See also: `Overview()` with parameter `details="analytics"`.
 282        """
 283
 284        self.overviewBondsCalendarFile = "overview-calendar.md"
 285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 286
 287        See also: `Overview()` with parameter `details="calendar"`.
 288        """
 289
 290        self.reportFile = "deals.md"
 291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 292
 293        See also: `Deals()`.
 294        """
 295
 296        self.withdrawalLimitsFile = "limits.md"
 297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 298
 299        See also: `OverviewLimits()` and `RequestLimits()`.
 300        """
 301
 302        self.userInfoFile = "user-info.md"
 303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 304
 305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 306        """
 307
 308        self.userAccountsFile = "accounts.md"
 309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 310
 311        See also: `OverviewAccounts()`, `RequestAccounts()`.
 312        """
 313
 314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 316
 317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 318
 319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 320        """
 321
 322        self.iList = None  # init iList for raw instruments data
 323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 324        
 325        See also: `Listing()`, `DumpInstruments()`.
 326        """
 327
 328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 329        if useCache:
 330            if os.path.exists(self.iListDumpFile):
 331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 332                curTime = datetime.now(tzutc())
 333
 334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 336
 337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 338
 339                else:
 340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 341
 342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 343                        os.path.abspath(self.iListDumpFile),
 344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 345                    ))
 346
 347            else:
 348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 350
 351        else:
 352            self.iList = self.Listing()  # request new raw instruments data from broker server
 353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 354
 355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 357
 358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 359        """
 360
 361    @property
 362    def ticker(self) -> str:
 363        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 364
 365        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 366        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 367
 368        See also: `SearchByTicker()`, `SearchInstruments()`.
 369        """
 370        return self._ticker
 371
 372    @ticker.setter
 373    def ticker(self, value):
 374        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 375
 376        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 377        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 378
 379        See also: `SearchByTicker()`, `SearchInstruments()`.
 380        """
 381        self._ticker = str(value).upper()  # Tickers may be upper case only
 382
 383    @property
 384    def figi(self) -> str:
 385        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 386
 387        See also: `SearchByFIGI()`, `SearchInstruments()`.
 388        """
 389        return self._figi
 390
 391    @figi.setter
 392    def figi(self, value):
 393        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 394
 395        See also: `SearchByFIGI()`, `SearchInstruments()`.
 396        """
 397        self._figi = str(value).upper()  # FIGI may be upper case only
 398
 399    def _ParseJSON(self, rawData="{}") -> dict:
 400        """
 401        Parse JSON from response string.
 402
 403        :param rawData: this is a string with JSON-formatted text.
 404        :return: JSON (dictionary), parsed from server response string.
 405        """
 406        responseJSON = json.loads(rawData) if rawData else {}
 407
 408        if self.moreDebug:
 409            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 410
 411        return responseJSON
 412
 413    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 414        """
 415        Send GET or POST request to broker server and receive JSON object.
 416
 417        self.header: must be defining with dictionary of headers.
 418        self.body: if define then used as request body. None by default.
 419        self.timeout: global request timeout, 15 seconds by default.
 420        :param url: url with REST request.
 421        :param reqType: send "GET" or "POST" request. "GET" by default.
 422        :param retry: how many times retry after first request if an 5xx server errors occurred.
 423        :param pause: sleep time in seconds between retries.
 424        :return: response JSON (dictionary) from broker.
 425        """
 426        if reqType.upper() not in ("GET", "POST"):
 427            uLogger.error("You can define request type: `GET` or `POST`!")
 428            raise Exception("Incorrect value")
 429
 430        if self.moreDebug:
 431            uLogger.debug("Request parameters:")
 432            uLogger.debug("    - REST API URL: {}".format(url))
 433            uLogger.debug("    - request type: {}".format(reqType))
 434            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 435            uLogger.debug("    - body:\n{}".format(self.body))
 436
 437        # fast hack to avoid all operations with some tickers/FIGI
 438        responseJSON = {}
 439        oK = True
 440        for item in self.exclude:
 441            if item in url:
 442                if self.moreDebug:
 443                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 444
 445                oK = False
 446                break
 447
 448        if oK:
 449            with self.__lock:  # acquire the mutex lock
 450                counter = 0
 451                response = None
 452                errMsg = ""
 453
 454                while not response and counter <= retry:
 455                    if reqType == "GET":
 456                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 457
 458                    if reqType == "POST":
 459                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 460
 461                    if self.moreDebug:
 462                        uLogger.debug("Response:")
 463                        uLogger.debug("    - status code: {}".format(response.status_code))
 464                        uLogger.debug("    - reason: {}".format(response.reason))
 465                        uLogger.debug("    - body length: {}".format(len(response.text)))
 466                        uLogger.debug("    - headers:\n{}".format(response.headers))
 467
 468                    # Server returns some headers:
 469                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 470                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 471                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 472                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 473                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 474                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 475                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 476                        sleep(rateLimitWait)
 477
 478                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 479                    if 400 <= response.status_code < 500:
 480                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 481                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 482
 483                        if "code" in response.text and "message" in response.text:
 484                            msgDict = self._ParseJSON(rawData=response.text)
 485                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 486
 487                        counter = retry + 1  # do not retry for 4xx errors
 488
 489                    if 500 <= response.status_code < 600:
 490                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 491                        uLogger.debug("    - not oK, {}".format(errMsg))
 492
 493                        if "code" in response.text and "message" in response.text:
 494                            errMsgDict = self._ParseJSON(rawData=response.text)
 495                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 496
 497                        counter += 1
 498
 499                        if counter <= retry:
 500                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 501                            sleep(pause)
 502
 503                responseJSON = self._ParseJSON(rawData=response.text)
 504
 505                if errMsg:
 506                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 507                    uLogger.error("    - not oK, {}".format(errMsg))
 508
 509        return responseJSON
 510
 511    def _IUpdater(self, iType: str) -> tuple:
 512        """
 513        Request instrument by type from server. See available API methods for instruments:
 514        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 515        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 516        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 517        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 518        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 519
 520        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 521        :return: tuple with iType name and list of available instruments of current type for defined user token.
 522        """
 523        result = []
 524
 525        if iType in TKS_INSTRUMENTS:
 526            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 527
 528            # all instruments have the same body in API v2 requests:
 529            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 530            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 531            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 532
 533        return iType, result
 534
 535    def _IWrapper(self, kwargs):
 536        """
 537        Wrapper runs instrument's update method `_IUpdater()`.
 538        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 539        """
 540        return self._IUpdater(**kwargs)
 541
 542    def Listing(self) -> dict:
 543        """
 544        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 545
 546        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 547        """
 548        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 549        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 550
 551        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 552        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 553        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 554
 555        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 556        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 557        poolUpdater.close()  # close the thread pool
 558        poolUpdater.join()  # wait a moment until all data returns from threads
 559
 560        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 561        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 562        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 563
 564        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 565        for iType in iList.keys():
 566            for ticker in iList[iType]:
 567                iList[iType][ticker]["type"] = iType
 568
 569                if "minPriceIncrement" in iList[iType][ticker].keys():
 570                    iList[iType][ticker]["step"] = NanoToFloat(
 571                        iList[iType][ticker]["minPriceIncrement"]["units"],
 572                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 573                    )
 574
 575                else:
 576                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 577
 578        return iList
 579
 580    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 581        """
 582        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 583
 584        See also: `DumpInstruments()`, `Listing()`.
 585
 586        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 587                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 588        """
 589        if self.iListDumpFile is None or not self.iListDumpFile:
 590            uLogger.error("Output name of dump file must be defined!")
 591            raise Exception("Filename required")
 592
 593        if not self.iList or forceUpdate:
 594            self.iList = self.Listing()
 595
 596        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 597
 598        # Save as XLSX with separated sheets for every type of instruments:
 599        with pd.ExcelWriter(
 600                path=xlsxDumpFile,
 601                date_format=TKS_DATE_FORMAT,
 602                datetime_format=TKS_DATE_TIME_FORMAT,
 603                mode="w",
 604        ) as writer:
 605            for iType in TKS_INSTRUMENTS:
 606                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 607                df = df[sorted(df)]  # sorted by column names
 608                df = df.applymap(
 609                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 610                    na_action="ignore",
 611                )  # converting numbers from nano-type to float in every cell
 612                df.to_excel(
 613                    writer,
 614                    sheet_name=iType,
 615                    encoding="UTF-8",
 616                    freeze_panes=(1, 1),
 617                )  # saving as XLSX-file with freeze first row and column as headers
 618
 619        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 620
 621    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 622        """
 623        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 624        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 625
 626        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 627
 628        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 629                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 630        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 631        """
 632        if self.iListDumpFile is None or not self.iListDumpFile:
 633            uLogger.error("Output name of dump file must be defined!")
 634            raise Exception("Filename required")
 635
 636        if not self.iList or forceUpdate:
 637            self.iList = self.Listing()
 638
 639        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 640        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 641            fH.write(jsonDump)
 642
 643        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 644
 645        return jsonDump
 646
 647    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 648        """
 649        Show information about one instrument defined by json data and prints it in Markdown format.
 650
 651        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 652
 653        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 654        :param show: if `True` then also printing information about instrument and its current price.
 655        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 656        :return: multilines text in Markdown format with information about one instrument.
 657        """
 658        splitLine = "|                                                             |                                                        |\n"
 659        infoText = ""
 660
 661        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 662            info = [
 663                "# Main information\n\n",
 664                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 665                "| Parameters                                                  | Values                                                 |\n",
 666                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 667                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 668                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 669            ]
 670
 671            if "sector" in iJSON.keys() and iJSON["sector"]:
 672                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 673
 674            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 675                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 676
 677            info.extend([
 678                splitLine,
 679                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 680                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 681            ])
 682
 683            if "isin" in iJSON.keys() and iJSON["isin"]:
 684                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 685
 686            if "classCode" in iJSON.keys():
 687                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 688
 689            info.extend([
 690                splitLine,
 691                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 692                splitLine,
 693                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 694                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 695                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 696            ])
 697
 698            if iJSON["figi"]:
 699                self._figi = iJSON["figi"]
 700                iJSON = iJSON | self.RequestTradingStatus()
 701
 702                info.extend([
 703                    splitLine,
 704                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 705                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 706                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 707                ])
 708
 709            info.append(splitLine)
 710
 711            if "type" in iJSON.keys() and iJSON["type"]:
 712                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 713
 714                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 715                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 716
 717            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 718                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 719
 720            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 721                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 722
 723            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 724                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 725
 726            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 727                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 728
 729            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 730                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 731
 732            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 733                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 734
 735            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 736                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 737
 738            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 739                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 740
 741            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 742                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 743
 744            if "currency" in iJSON.keys():
 745                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 746
 747            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 748                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 749
 750            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 751                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 752
 753            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 754                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 755
 756            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 757                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 758
 759            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 760                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 761
 762            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 763                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 764
 765            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 766                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 767
 768            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 769                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 770
 771            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 772                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 773
 774            iExt = None
 775            if iJSON["type"] == "Bonds":
 776                info.extend([
 777                    splitLine,
 778                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 779                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 780                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 781                        iJSON["nominal"]["currency"],
 782                    )),
 783                ])
 784
 785                if "floatingCouponFlag" in iJSON.keys():
 786                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 787
 788                if "amortizationFlag" in iJSON.keys():
 789                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 790
 791                info.append(splitLine)
 792
 793                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 794                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 795
 796                if iJSON["figi"]:
 797                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 798
 799                    info.extend([
 800                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 801                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 802                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 803                    ])
 804
 805                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 806                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 807                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 808                        iJSON["aciValue"]["currency"]
 809                    )))
 810
 811            if "currentPrice" in iJSON.keys():
 812                info.append(splitLine)
 813
 814                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 815                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 816
 817                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 818                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 819                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 820                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 821                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 822
 823                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 824                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 825
 826                info.extend([
 827                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 828                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 829                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 830                    )),
 831                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 832                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 833                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 834                    )),
 835                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 836                        "{:.2f}%{}".format(
 837                            iJSON["currentPrice"]["changes"],
 838                            " ({}{:.2f} {})".format(
 839                                "+" if bondChangesDelta > 0 else "",
 840                                bondChangesDelta,
 841                                aciCurrency
 842                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 843                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 844                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 845                                currency
 846                            ),
 847                        )
 848                    ),
 849                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 850                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 851                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 852                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 853                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 854                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 855                    )),
 856                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 857                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 858                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 859                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 860                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 861                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 862                    )),
 863                ])
 864
 865            if "lot" in iJSON.keys():
 866                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 867
 868            if "step" in iJSON.keys() and iJSON["step"] != 0:
 869                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 870
 871            # Add bond payment calendar:
 872            if iJSON["type"] == "Bonds":
 873                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 874                info.extend(["\n#", strCalendar])
 875
 876            infoText += "".join(info)
 877
 878            if show and not onlyFiles:
 879                uLogger.info("{}".format(infoText))
 880
 881            if self.infoFile is not None and (show or onlyFiles):
 882                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 883                    fH.write(infoText)
 884
 885                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 886
 887                if self.useHTMLReports:
 888                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 889                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 890                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 891
 892                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 893
 894        return infoText
 895
 896    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 897        """
 898        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 899
 900        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 901        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 902        :return: JSON formatted data with information about instrument.
 903        """
 904        tickerJSON = {}
 905        if self.moreDebug:
 906            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 907
 908        if not self._ticker:
 909            uLogger.warning("self._ticker variable is not be empty!")
 910
 911        else:
 912            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 913                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 914                raise Exception("Instrument not allowed")
 915
 916            if not self.iList:
 917                self.iList = self.Listing()
 918
 919            if self._ticker in self.iList["Shares"].keys():
 920                tickerJSON = self.iList["Shares"][self._ticker]
 921                if self.moreDebug:
 922                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 923
 924            elif self._ticker in self.iList["Currencies"].keys():
 925                tickerJSON = self.iList["Currencies"][self._ticker]
 926                if self.moreDebug:
 927                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 928
 929            elif self._ticker in self.iList["Bonds"].keys():
 930                tickerJSON = self.iList["Bonds"][self._ticker]
 931                if self.moreDebug:
 932                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 933
 934            elif self._ticker in self.iList["Etfs"].keys():
 935                tickerJSON = self.iList["Etfs"][self._ticker]
 936                if self.moreDebug:
 937                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 938
 939            elif self._ticker in self.iList["Futures"].keys():
 940                tickerJSON = self.iList["Futures"][self._ticker]
 941                if self.moreDebug:
 942                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 943
 944        if tickerJSON:
 945            self._figi = tickerJSON["figi"]
 946
 947            if requestPrice:
 948                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 949
 950                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 951                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 952
 953                else:
 954                    tickerJSON["currentPrice"]["changes"] = 0
 955
 956            if show:
 957                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 958
 959        else:
 960            if show:
 961                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 962
 963        return tickerJSON
 964
 965    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 966        """
 967        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 968
 969        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 970        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 971        :return: JSON formatted data with information about instrument.
 972        """
 973        figiJSON = {}
 974        if self.moreDebug:
 975            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 976
 977        if not self._figi:
 978            uLogger.warning("self._figi variable is not be empty!")
 979
 980        else:
 981            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 982                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 983                raise Exception("Instrument not allowed")
 984
 985            if not self.iList:
 986                self.iList = self.Listing()
 987
 988            for item in self.iList["Shares"].keys():
 989                if self._figi == self.iList["Shares"][item]["figi"]:
 990                    figiJSON = self.iList["Shares"][item]
 991
 992                    if self.moreDebug:
 993                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 994
 995                    break
 996
 997            if not figiJSON:
 998                for item in self.iList["Currencies"].keys():
 999                    if self._figi == self.iList["Currencies"][item]["figi"]:
1000                        figiJSON = self.iList["Currencies"][item]
1001
1002                        if self.moreDebug:
1003                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1004
1005                        break
1006
1007            if not figiJSON:
1008                for item in self.iList["Bonds"].keys():
1009                    if self._figi == self.iList["Bonds"][item]["figi"]:
1010                        figiJSON = self.iList["Bonds"][item]
1011
1012                        if self.moreDebug:
1013                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1014
1015                        break
1016
1017            if not figiJSON:
1018                for item in self.iList["Etfs"].keys():
1019                    if self._figi == self.iList["Etfs"][item]["figi"]:
1020                        figiJSON = self.iList["Etfs"][item]
1021
1022                        if self.moreDebug:
1023                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1024
1025                        break
1026
1027            if not figiJSON:
1028                for item in self.iList["Futures"].keys():
1029                    if self._figi == self.iList["Futures"][item]["figi"]:
1030                        figiJSON = self.iList["Futures"][item]
1031
1032                        if self.moreDebug:
1033                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1034
1035                        break
1036
1037        if figiJSON:
1038            self._figi = figiJSON["figi"]
1039            self._ticker = figiJSON["ticker"]
1040
1041            if requestPrice:
1042                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1043
1044                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1045                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1046
1047                else:
1048                    figiJSON["currentPrice"]["changes"] = 0
1049
1050            if show:
1051                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1052
1053        else:
1054            if show:
1055                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1056
1057        return figiJSON
1058
1059    def GetCurrentPrices(self, show: bool = True) -> dict:
1060        """
1061        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1062        `{"buy": [{"price": 1243.8, "quantity": 193},
1063                  {"price": 1244.0, "quantity": 168},
1064                  {"price": 1244.8, "quantity": 5},
1065                  {"price": 1245.0, "quantity": 61},
1066                  {"price": 1245.4, "quantity": 60}],
1067          "sell": [{"price": 1243.6, "quantity": 8},
1068                   {"price": 1242.6, "quantity": 10},
1069                   {"price": 1242.4, "quantity": 18},
1070                   {"price": 1242.2, "quantity": 50},
1071                   {"price": 1242.0, "quantity": 113}],
1072          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1073        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1074        - sell: list of dicts with Buyers prices,
1075            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1076            - quantity: volume value by current price in lots,
1077        - limitUp: current trade session limit price, maximum,
1078        - limitDown: current trade session limit price, minimum,
1079        - lastPrice: last deal price of the instrument,
1080        - closePrice: previous trade session close price of the instrument.
1081
1082        See also: `SearchByTicker()` and `SearchByFIGI()`.
1083        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1084        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1085
1086        :param show: if `True` then print DOM to log and console.
1087        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1088                 If an error occurred then returns an empty record:
1089                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1090        """
1091        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1092
1093        if self.depth < 1:
1094            uLogger.error("Depth of Market (DOM) must be >=1!")
1095            raise Exception("Incorrect value")
1096
1097        if not (self._ticker or self._figi):
1098            uLogger.error("self._ticker or self._figi variables must be defined!")
1099            raise Exception("Ticker or FIGI required")
1100
1101        if self._ticker and not self._figi:
1102            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1103            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1104
1105        if not self._ticker and self._figi:
1106            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1107            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1108
1109        if not self._figi:
1110            uLogger.error("FIGI is not defined!")
1111            raise Exception("Ticker or FIGI required")
1112
1113        else:
1114            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1115
1116            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1117            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1118            self.body = str({"figi": self._figi, "depth": self.depth})
1119            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1120
1121            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1122                # list of dicts with sellers orders:
1123                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1124
1125                # list of dicts with buyers orders:
1126                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1127
1128                # max price of instrument at this time:
1129                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1130
1131                # min price of instrument at this time:
1132                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1133
1134                # last price of deal with instrument:
1135                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1136
1137                # last close price of instrument:
1138                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1139
1140            else:
1141                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1142                uLogger.debug("Server response: {}".format(pricesResponse))
1143
1144            if show:
1145                if prices["buy"] or prices["sell"]:
1146                    info = [
1147                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1148                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1149                            self._ticker,
1150                            self._figi,
1151                            self.depth,
1152                        ),
1153                        "-" * 60, "\n",
1154                        "             Orders of Buyers | Orders of Sellers\n",
1155                        "-" * 60, "\n",
1156                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1157                        "-" * 60, "\n",
1158                    ]
1159
1160                    if not prices["buy"]:
1161                        info.append("                              | No orders!\n")
1162                        sumBuy = 0
1163
1164                    else:
1165                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1166                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1167                        for item in maxMinSorted:
1168                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1169
1170                    if not prices["sell"]:
1171                        info.append("No orders!                    |\n")
1172                        sumSell = 0
1173
1174                    else:
1175                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1176                        for item in prices["sell"]:
1177                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1178
1179                    info.extend([
1180                        "-" * 60, "\n",
1181                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1182                        "-" * 60, "\n",
1183                    ])
1184
1185                    infoText = "".join(info)
1186
1187                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1188
1189                else:
1190                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1191
1192        return prices
1193
1194    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1195        """
1196        This method get and show information about all available broker instruments for current user account.
1197        If `instrumentsFile` string is not empty then also save information to this file.
1198
1199        :param show: if `True` then print results to console, if `False` — print only to file.
1200        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1201        :return: multi-lines string with all available broker instruments.
1202        """
1203        if not self.iList:
1204            self.iList = self.Listing()
1205
1206        info = [
1207            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1208            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1209        ]
1210
1211        # add instruments count by type:
1212        for iType in self.iList.keys():
1213            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1214
1215        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1216        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1217
1218        # generating info tables with all instruments by type:
1219        for iType in self.iList.keys():
1220            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1221
1222            for instrument in self.iList[iType].keys():
1223                iName = self.iList[iType][instrument]["name"]  # instrument's name
1224                if len(iName) > 57:
1225                    iName = "{}...".format(iName[:54])  # right trim for a long string
1226
1227                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1228                    self.iList[iType][instrument]["ticker"],
1229                    iName,
1230                    self.iList[iType][instrument]["figi"],
1231                    self.iList[iType][instrument]["currency"],
1232                    self.iList[iType][instrument]["lot"],
1233                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1234                ))
1235
1236        infoText = "".join(info)
1237
1238        if show and not onlyFiles:
1239            uLogger.info(infoText)
1240
1241        if self.instrumentsFile and (show or onlyFiles):
1242            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1243                fH.write(infoText)
1244
1245            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1246
1247            if self.useHTMLReports:
1248                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1249                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1250                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1251
1252                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1253
1254        return infoText
1255
1256    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1257        """
1258        This method search and show information about instruments by part of its ticker, FIGI or name.
1259        If `searchResultsFile` string is not empty then also save information to this file.
1260
1261        :param pattern: string with part of ticker, FIGI or instrument's name.
1262        :param show: if `True` then print results to console, if `False` — return list of result only.
1263        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1264        :return: list of dictionaries with all found instruments.
1265        """
1266        if not self.iList:
1267            self.iList = self.Listing()
1268
1269        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1270        compiledPattern = re.compile(pattern, re.IGNORECASE)
1271
1272        for iType in self.iList:
1273            for instrument in self.iList[iType].values():
1274                searchResult = compiledPattern.search(" ".join(
1275                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1276                ))
1277
1278                if searchResult:
1279                    searchResults[iType][instrument["ticker"]] = instrument
1280
1281        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1282        info = [
1283            "# Search results\n\n",
1284            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1285            "* **Search pattern:** [{}]\n".format(pattern),
1286            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1287            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1288        ]
1289        infoShort = info[:]
1290
1291        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1292        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1293        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1294
1295        if resultsLen == 0:
1296            info.append("\nNo results\n")
1297            infoShort.append("\nNo results\n")
1298            uLogger.warning("No results. Try changing your search pattern.")
1299
1300        else:
1301            for iType in searchResults:
1302                iTypeValuesCount = len(searchResults[iType].values())
1303                if iTypeValuesCount > 0:
1304                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1305                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1306
1307                    for instrument in searchResults[iType].values():
1308                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1309                            instrument["type"],
1310                            instrument["ticker"],
1311                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1312                            instrument["figi"],
1313                        ))
1314
1315                    if iTypeValuesCount <= 5:
1316                        infoShort.extend(info[-iTypeValuesCount:])
1317
1318                    else:
1319                        infoShort.extend(info[-5:])
1320                        infoShort.append(skippedLine)
1321
1322        infoText = "".join(info)
1323        infoTextShort = "".join(infoShort)
1324
1325        if show and not onlyFiles:
1326            uLogger.info(infoTextShort)
1327            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1328
1329        if self.searchResultsFile and (show or onlyFiles):
1330            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1331                fH.write(infoText)
1332
1333            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1334
1335            if self.useHTMLReports:
1336                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1337                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1338                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1339
1340                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1341
1342        return searchResults
1343
1344    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1345        """
1346        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1347
1348        :param instruments: list of strings with tickers or FIGIs.
1349        :return: list with unique instrument FIGIs only.
1350        """
1351        requestedInstruments = []
1352        for iName in instruments:
1353            if iName not in self.aliases.keys():
1354                if iName not in requestedInstruments:
1355                    requestedInstruments.append(iName)
1356
1357            else:
1358                if iName not in requestedInstruments:
1359                    if self.aliases[iName] not in requestedInstruments:
1360                        requestedInstruments.append(self.aliases[iName])
1361
1362        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1363
1364        onlyUniqueFIGIs = []
1365        for iName in requestedInstruments:
1366            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1367                continue
1368
1369            self._ticker = iName
1370            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1371
1372            if not iData:
1373                self._ticker = ""
1374                self._figi = iName
1375
1376                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1377
1378                if not iData:
1379                    self._figi = ""
1380                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1381
1382            if iData and iData["figi"] not in onlyUniqueFIGIs:
1383                onlyUniqueFIGIs.append(iData["figi"])
1384
1385        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1386
1387        return onlyUniqueFIGIs
1388
1389    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1390        """
1391        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1392
1393        See limits: https://tinkoff.github.io/investAPI/limits/
1394
1395        If `pricesFile` string is not empty then also save information to this file.
1396
1397        :param instruments: list of strings with tickers or FIGIs.
1398        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1399        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1400        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1401                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1402        """
1403        if instruments is None or not instruments:
1404            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1405            raise Exception("Ticker or FIGI required")
1406
1407        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1408
1409        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1410
1411        iList = []  # trying to get info and current prices about all unique instruments:
1412        for self._figi in onlyUniqueFIGIs:
1413            iData = self.SearchByFIGI(requestPrice=True, show=False)
1414            iList.append(iData)
1415
1416        self.ShowListOfPrices(iList, show, onlyFiles)
1417
1418        return iList
1419
1420    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1421        """
1422        Show table contains current prices of given instruments.
1423
1424        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1425                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1426        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1427        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1428        :return: multilines text in Markdown format as a table contains current prices.
1429        """
1430        infoText = ""
1431
1432        if show or self.pricesFile or onlyFiles:
1433            info = [
1434                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1435                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1436                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1437            ]
1438
1439            for item in iList:
1440                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1441                    item["ticker"],
1442                    item["figi"],
1443                    item["type"],
1444                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1445                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1446                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1447                    "{} / {}".format(
1448                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1449                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1450                    ),
1451                    "{} / {}".format(
1452                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1453                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1454                    ),
1455                    item["currency"],
1456                ))
1457
1458            infoText = "".join(info)
1459
1460            if show and not onlyFiles:
1461                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1462
1463            if self.pricesFile and (show or onlyFiles):
1464                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1465                    fH.write(infoText)
1466
1467                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1468
1469                if self.useHTMLReports:
1470                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1471                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1472                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1473
1474                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1475
1476        return infoText
1477
1478    def RequestTradingStatus(self) -> dict:
1479        """
1480        Requesting trading status for the instrument defined by `figi` variable.
1481
1482        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1483
1484        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1485
1486        :return: dictionary with trading status attributes. Response example:
1487                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1488                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1489        """
1490        if self._figi is None or not self._figi:
1491            uLogger.error("Variable `figi` must be defined for using this method!")
1492            raise Exception("FIGI required")
1493
1494        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1495
1496        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1497        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1498        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1499
1500        if self.moreDebug:
1501            uLogger.debug("Records about current trading status successfully received")
1502
1503        return tradingStatus
1504
1505    def RequestPortfolio(self) -> dict:
1506        """
1507        Requesting actual user's portfolio for current `accountId`.
1508
1509        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1510
1511        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1512
1513        :return: dictionary with user's portfolio.
1514        """
1515        if self.accountId is None or not self.accountId:
1516            uLogger.error("Variable `accountId` must be defined for using this method!")
1517            raise Exception("Account ID required")
1518
1519        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1520
1521        self.body = str({"accountId": self.accountId})
1522        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1523        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1524
1525        if self.moreDebug:
1526            uLogger.debug("Records about user's portfolio successfully received")
1527
1528        return rawPortfolio
1529
1530    def RequestPositions(self) -> dict:
1531        """
1532        Requesting open positions by currencies and instruments for current `accountId`.
1533
1534        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1535
1536        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1537
1538        :return: dictionary with open positions by instruments.
1539        """
1540        if self.accountId is None or not self.accountId:
1541            uLogger.error("Variable `accountId` must be defined for using this method!")
1542            raise Exception("Account ID required")
1543
1544        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1545
1546        self.body = str({"accountId": self.accountId})
1547        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1548        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1549
1550        if self.moreDebug:
1551            uLogger.debug("Records about current open positions successfully received")
1552
1553        return rawPositions
1554
1555    def RequestPendingOrders(self) -> list:
1556        """
1557        Requesting current actual pending limit orders for current `accountId`.
1558
1559        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1560
1561        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1562
1563        :return: list of dictionaries with pending limit orders.
1564        """
1565        if self.accountId is None or not self.accountId:
1566            uLogger.error("Variable `accountId` must be defined for using this method!")
1567            raise Exception("Account ID required")
1568
1569        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1570
1571        self.body = str({"accountId": self.accountId})
1572        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1573        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1574
1575        if "orders" in rawResponse.keys():
1576            rawOrders = rawResponse["orders"]
1577            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1578
1579        else:
1580            rawOrders = []
1581            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1582
1583        return rawOrders
1584
1585    def RequestStopOrders(self) -> list:
1586        """
1587        Requesting current actual stop orders for current `accountId`.
1588
1589        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1590
1591        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1592
1593        :return: list of dictionaries with stop orders.
1594        """
1595        if self.accountId is None or not self.accountId:
1596            uLogger.error("Variable `accountId` must be defined for using this method!")
1597            raise Exception("Account ID required")
1598
1599        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1600
1601        self.body = str({"accountId": self.accountId})
1602        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1603        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1604
1605        if "stopOrders" in rawResponse.keys():
1606            rawStopOrders = rawResponse["stopOrders"]
1607            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1608
1609        else:
1610            rawStopOrders = []
1611            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1612
1613        return rawStopOrders
1614
1615    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1616        """
1617        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1618        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1619        and `overviewBondsCalendarFile` are defined then also save information to file.
1620
1621        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1622        many requests about the state of the portfolio, and then, based on the received data, a large number
1623        of calculation and statistics are collected.
1624
1625        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1626        :param details: how detailed should the information be?
1627        - `full` — shows full available information about portfolio status (by default),
1628        - `positions` — shows only open positions,
1629        - `orders` — shows only sections of open limits and stop orders.
1630        - `digest` — show a short digest of the portfolio status,
1631        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1632        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1633        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1634        :return: dictionary with client's raw portfolio and some statistics.
1635        """
1636        if self.accountId is None or not self.accountId:
1637            uLogger.error("Variable `accountId` must be defined for using this method!")
1638            raise Exception("Account ID required")
1639
1640        view = {
1641            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1642                "headers": {},  # list of dictionaries, response headers without "positions" section
1643                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1644                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1645                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1646                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1647                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1648                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1649                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1650                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1651                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1652            },
1653            "stat": {  # --- some statistics calculated using "raw" sections:
1654                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1655                "availableRUB": 0.,  # available rubles (without other currencies)
1656                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1657                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1658                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1659                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1660                "sharesCostRUB": 0.,  # costs of all shares in RUB
1661                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1662                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1663                "futuresCostRUB": 0.,  # costs of all futures in RUB
1664                "Currencies": [],  # list of dictionaries of all currencies statistics
1665                "Shares": [],  # list of dictionaries of all shares statistics
1666                "Bonds": [],  # list of dictionaries of all bonds statistics
1667                "Etfs": [],  # list of dictionaries of all etfs statistics
1668                "Futures": [],  # list of dictionaries of all futures statistics
1669                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1670                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1671                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1672                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1673                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1674            },
1675            "analytics": {  # --- some analytics of portfolio:
1676                "distrByAssets": {},  # portfolio distribution by assets
1677                "distrByCompanies": {},  # portfolio distribution by companies
1678                "distrBySectors": {},  # portfolio distribution by sectors
1679                "distrByCurrencies": {},  # portfolio distribution by currencies
1680                "distrByCountries": {},  # portfolio distribution by countries
1681                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1682            }
1683        }
1684
1685        details = details.lower()
1686        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1687        if details not in availableDetails:
1688            details = "full"
1689            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1690
1691        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1692
1693        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1694        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1695        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1696        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1697
1698        # save response headers without "positions" section:
1699        for key in portfolioResponse.keys():
1700            if key != "positions":
1701                view["raw"]["headers"][key] = portfolioResponse[key]
1702
1703            else:
1704                continue
1705
1706        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1707        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1708        for item in portfolioResponse["positions"]:
1709            if item["instrumentType"] == "currency":
1710                self._figi = item["figi"]
1711                if not self._figi and item["ticker"]:
1712                    self._ticker = item["ticker"]
1713                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1714
1715                curr = self.SearchByFIGI(requestPrice=False)
1716
1717                # current price of currency in RUB:
1718                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1719                    "name": curr["name"],
1720                    "currentPrice": NanoToFloat(
1721                        item["currentPrice"]["units"],
1722                        item["currentPrice"]["nano"]
1723                    ),
1724                }
1725
1726                view["raw"]["Currencies"].append(item)
1727
1728            elif item["instrumentType"] == "share":
1729                view["raw"]["Shares"].append(item)
1730
1731            elif item["instrumentType"] == "bond":
1732                view["raw"]["Bonds"].append(item)
1733
1734            elif item["instrumentType"] == "etf":
1735                view["raw"]["Etfs"].append(item)
1736
1737            elif item["instrumentType"] == "futures":
1738                view["raw"]["Futures"].append(item)
1739
1740            else:
1741                continue
1742
1743        # how many volume of currencies (by ISO currency name) are blocked:
1744        for item in view["raw"]["positions"]["blocked"]:
1745            blocked = NanoToFloat(item["units"], item["nano"])
1746            if blocked > 0:
1747                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1748
1749        # how many volume of instruments (by FIGI) are blocked:
1750        for item in view["raw"]["positions"]["securities"]:
1751            blocked = int(item["blocked"])
1752            if blocked > 0:
1753                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1754
1755        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1756
1757        if "rub" in allBlocked.keys():
1758            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1759
1760        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1761        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1762        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1763        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1764        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1765        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1766        view["stat"]["portfolioCostRUB"] = sum([
1767            view["stat"]["allCurrenciesCostRUB"],
1768            view["stat"]["sharesCostRUB"],
1769            view["stat"]["bondsCostRUB"],
1770            view["stat"]["etfsCostRUB"],
1771            view["stat"]["futuresCostRUB"],
1772        ])
1773
1774        # --- calculating some portfolio statistics:
1775        byComp = {}  # distribution by companies
1776        bySect = {}  # distribution by sectors
1777        byCurr = {}  # distribution by currencies (include RUB)
1778        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1779        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1780
1781        for item in portfolioResponse["positions"]:
1782            self._figi = item["figi"]
1783            if not self._figi and item["ticker"]:
1784                self._ticker = item["ticker"]
1785                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1786
1787            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1788
1789            if instrument:
1790                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1791                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1792
1793                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1794                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1795
1796                else:
1797                    blocked = 0
1798
1799                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1800                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1801                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1802                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1803                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1804                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1805                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1806                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1807                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1808                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1809                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1810                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1811
1812                statData = {
1813                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1814                    "ticker": instrument["ticker"],  # ticker by FIGI
1815                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1816                    "volume": volume,  # available volume of instrument
1817                    "lots": lots,  # volume in lots of instrument
1818                    "direction": direction,  # direction of an instrument's position: short or long
1819                    "blocked": blocked,  # blocked volume of currency or instrument
1820                    "currentPrice": curPrice,  # current instrument's price in basic asset
1821                    "average": average,  # current average position price
1822                    "cost": cost,  # current cost of all volume of instrument in basic asset
1823                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1824                    "costRUB": costRUB,  # cost of instrument in ruble
1825                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1826                    "profit": profit,  # expected profit at current moment
1827                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1828                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1829                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1830                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1831                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1832                    "step": instrument["step"],  # minimum price increment
1833                }
1834
1835                # adding distribution by unique countries:
1836                if statData["country"] not in byCountry.keys():
1837                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1838
1839                else:
1840                    byCountry[statData["country"]]["cost"] += costRUB
1841                    byCountry[statData["country"]]["percent"] += percentCostRUB
1842
1843                if item["instrumentType"] != "currency":
1844                    # adding distribution by unique companies:
1845                    if statData["name"]:
1846                        if statData["name"] not in byComp.keys():
1847                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1848
1849                        else:
1850                            byComp[statData["name"]]["cost"] += costRUB
1851                            byComp[statData["name"]]["percent"] += percentCostRUB
1852
1853                    # adding distribution by unique sectors:
1854                    if statData["sector"] not in bySect.keys():
1855                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1856
1857                    else:
1858                        bySect[statData["sector"]]["cost"] += costRUB
1859                        bySect[statData["sector"]]["percent"] += percentCostRUB
1860
1861                # adding distribution by unique currencies:
1862                if currency not in byCurr.keys():
1863                    byCurr[currency] = {
1864                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1865                        "cost": costRUB,
1866                        "percent": percentCostRUB
1867                    }
1868
1869                else:
1870                    byCurr[currency]["cost"] += costRUB
1871                    byCurr[currency]["percent"] += percentCostRUB
1872
1873                # saving statistics for every instrument:
1874                if item["instrumentType"] == "currency":
1875                    view["stat"]["Currencies"].append(statData)
1876
1877                    # update dict with free funds for trading (total - blocked) by currencies
1878                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1879                    view["stat"]["funds"][currency] = {
1880                        "total": volume,
1881                        "totalCostRUB": costRUB,  # total volume cost in rubles
1882                        "free": volume - blocked,
1883                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1884                    }
1885
1886                elif item["instrumentType"] == "share":
1887                    view["stat"]["Shares"].append(statData)
1888
1889                elif item["instrumentType"] == "bond":
1890                    view["stat"]["Bonds"].append(statData)
1891
1892                elif item["instrumentType"] == "etf":
1893                    view["stat"]["Etfs"].append(statData)
1894
1895                elif item["instrumentType"] == "Futures":
1896                    view["stat"]["Futures"].append(statData)
1897
1898                else:
1899                    continue
1900
1901        # total changes in Russian Ruble:
1902        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1903        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1904        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1905        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1906        view["stat"]["funds"]["rub"] = {
1907            "total": view["stat"]["availableRUB"],
1908            "totalCostRUB": view["stat"]["availableRUB"],
1909            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1910            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1911        }
1912
1913        # --- pending limit orders sector data:
1914        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1915        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1916
1917        for item in view["raw"]["orders"]:
1918            self._figi = item["figi"]
1919
1920            if item["figi"] not in uniquePendingOrdersFIGIs:
1921                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1922
1923                uniquePendingOrdersFIGIs.append(item["figi"])
1924                uniquePendingOrders[item["figi"]] = instrument
1925
1926            else:
1927                instrument = uniquePendingOrders[item["figi"]]
1928
1929            if instrument:
1930                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1931                orderType = TKS_ORDER_TYPES[item["orderType"]]
1932                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1933                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1934
1935                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1936                if item["direction"] == "ORDER_DIRECTION_BUY":
1937                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1938
1939                else:
1940                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1941
1942                # requested price for order execution:
1943                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1944
1945                # necessary changes in percent to reach target from current price:
1946                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1947
1948                view["stat"]["orders"].append({
1949                    "orderID": item["orderId"],  # orderId number parameter of current order
1950                    "figi": item["figi"],  # FIGI identification
1951                    "ticker": instrument["ticker"],  # ticker name by FIGI
1952                    "lotsRequested": item["lotsRequested"],  # requested lots value
1953                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1954                    "currentPrice": lastPrice,  # current instrument's price for defined action
1955                    "targetPrice": target,  # requested price for order execution in base currency
1956                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1957                    "percentChanges": changes,  # changes in percent to target from current price
1958                    "currency": item["currency"],  # instrument's currency name
1959                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1960                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1961                    "status": orderState,  # order status from TKS_ORDER_STATES
1962                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1963                })
1964
1965        # --- stop orders sector data:
1966        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1967        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1968
1969        for item in view["raw"]["stopOrders"]:
1970            self._figi = item["figi"]
1971
1972            if item["figi"] not in uniqueStopOrdersFIGIs:
1973                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1974
1975                uniqueStopOrdersFIGIs.append(item["figi"])
1976                uniqueStopOrders[item["figi"]] = instrument
1977
1978            else:
1979                instrument = uniqueStopOrders[item["figi"]]
1980
1981            if instrument:
1982                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1983                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1984                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1985
1986                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1987                if "expirationTime" in item.keys():
1988                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1989                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1990
1991                else:
1992                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1993                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1994
1995                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1996                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1997                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1998
1999                else:
2000                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2001
2002                # requested price when stop-order executed:
2003                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2004
2005                # price for limit-order, set up when stop-order executed:
2006                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2007
2008                # necessary changes in percent to reach target from current price:
2009                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2010
2011                view["stat"]["stopOrders"].append({
2012                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2013                    "figi": item["figi"],  # FIGI identification
2014                    "ticker": instrument["ticker"],  # ticker name by FIGI
2015                    "lotsRequested": item["lotsRequested"],  # requested lots value
2016                    "currentPrice": lastPrice,  # current instrument's price for defined action
2017                    "targetPrice": target,  # requested price for stop-order execution in base currency
2018                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2019                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2020                    "percentChanges": changes,  # changes in percent to target from current price
2021                    "currency": item["currency"],  # instrument's currency name
2022                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2023                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2024                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2025                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2026                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2027                })
2028
2029        # --- calculating data for analytics section:
2030        # portfolio distribution by assets:
2031        view["analytics"]["distrByAssets"] = {
2032            "Ruble": {
2033                "uniques": 1,
2034                "cost": view["stat"]["availableRUB"],
2035                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2036            },
2037            "Currencies": {
2038                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2039                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2040                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2041            },
2042            "Shares": {
2043                "uniques": len(view["stat"]["Shares"]),
2044                "cost": view["stat"]["sharesCostRUB"],
2045                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2046            },
2047            "Bonds": {
2048                "uniques": len(view["stat"]["Bonds"]),
2049                "cost": view["stat"]["bondsCostRUB"],
2050                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2051            },
2052            "Etfs": {
2053                "uniques": len(view["stat"]["Etfs"]),
2054                "cost": view["stat"]["etfsCostRUB"],
2055                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2056            },
2057            "Futures": {
2058                "uniques": len(view["stat"]["Futures"]),
2059                "cost": view["stat"]["futuresCostRUB"],
2060                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2061            },
2062        }
2063
2064        # portfolio distribution by companies:
2065        view["analytics"]["distrByCompanies"]["All money cash"] = {
2066            "ticker": "",
2067            "cost": view["stat"]["allCurrenciesCostRUB"],
2068            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2069        }
2070        view["analytics"]["distrByCompanies"].update(byComp)
2071
2072        # portfolio distribution by sectors:
2073        view["analytics"]["distrBySectors"]["All money cash"] = {
2074            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2075            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2076        }
2077        view["analytics"]["distrBySectors"].update(bySect)
2078
2079        # portfolio distribution by currencies:
2080        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2081            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2082
2083            if self.moreDebug:
2084                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2085
2086        view["analytics"]["distrByCurrencies"].update(byCurr)
2087        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2088        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2089
2090        # portfolio distribution by countries:
2091        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2092            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2093
2094            if self.moreDebug:
2095                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2096
2097        view["analytics"]["distrByCountries"].update(byCountry)
2098        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2099        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2100
2101        # --- Prepare text statistics overview in human-readable:
2102        if show or onlyFiles:
2103            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2104
2105            # Whatever the value `details`, header not changes:
2106            info = [
2107                "# Client's portfolio\n\n",
2108                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2109                "* **Account ID:** [{}]\n".format(self.accountId),
2110            ]
2111
2112            if details in ["full", "positions", "digest"]:
2113                info.extend([
2114                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2115                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2116                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2117                        view["stat"]["totalChangesRUB"],
2118                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2119                        view["stat"]["totalChangesPercentRUB"],
2120                    ),
2121                ])
2122
2123            if details in ["full", "positions"]:
2124                info.extend([
2125                    "## Open positions\n\n",
2126                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2127                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2128                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2129                        "{:.2f} ({:.2f}) rub".format(
2130                            view["stat"]["availableRUB"],
2131                            view["stat"]["blockedRUB"],
2132                        )
2133                    )
2134                ])
2135
2136                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2137                    return [
2138                        "|                             |                                 |          |              |              |                     |                              |\n",
2139                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2140                            noTradeStr if noTradeStr else typeStr,
2141                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2142                        ),
2143                    ]
2144
2145                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2146                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2147                        "{} [{}]".format(data["ticker"], data["figi"]),
2148                        "{:.2f} ({:.2f}) {}".format(
2149                            data["volume"],
2150                            data["blocked"],
2151                            data["currency"],
2152                        ) if isCurr else "{:.0f} ({:.0f})".format(
2153                            data["volume"],
2154                            data["blocked"],
2155                        ),
2156                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2157                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2158                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2159                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2160                        "{}{:.2f} {} ({}{:.2f}%)".format(
2161                            "+" if data["profit"] > 0 else "",
2162                            data["profit"], data["baseCurrencyName"],
2163                            "+" if data["percentProfit"] > 0 else "",
2164                            data["percentProfit"],
2165                        ),
2166                    )
2167
2168                # --- Show currencies section:
2169                if view["stat"]["Currencies"]:
2170                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2171                    for item in view["stat"]["Currencies"]:
2172                        info.append(_InfoStr(item, isCurr=True))
2173
2174                else:
2175                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2176
2177                # --- Show shares section:
2178                if view["stat"]["Shares"]:
2179                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2180
2181                    for item in view["stat"]["Shares"]:
2182                        info.append(_InfoStr(item))
2183
2184                else:
2185                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2186
2187                # --- Show bonds section:
2188                if view["stat"]["Bonds"]:
2189                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2190
2191                    for item in view["stat"]["Bonds"]:
2192                        info.append(_InfoStr(item))
2193
2194                else:
2195                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2196
2197                # --- Show etfs section:
2198                if view["stat"]["Etfs"]:
2199                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2200
2201                    for item in view["stat"]["Etfs"]:
2202                        info.append(_InfoStr(item))
2203
2204                else:
2205                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2206
2207                # --- Show futures section:
2208                if view["stat"]["Futures"]:
2209                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2210
2211                    for item in view["stat"]["Futures"]:
2212                        info.append(_InfoStr(item))
2213
2214                else:
2215                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2216
2217            if details in ["full", "orders"]:
2218                # --- Show pending limit orders section:
2219                if view["stat"]["orders"]:
2220                    info.extend([
2221                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2222                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2223                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2224                    ])
2225
2226                    for item in view["stat"]["orders"]:
2227                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2228                            "{} [{}]".format(item["ticker"], item["figi"]),
2229                            item["orderID"],
2230                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2231                            "{} {} ({}{:.2f}%)".format(
2232                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2233                                item["baseCurrencyName"],
2234                                "+" if item["percentChanges"] > 0 else "",
2235                                float(item["percentChanges"]),
2236                            ),
2237                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2238                            item["action"],
2239                            item["type"],
2240                            item["date"],
2241                        ))
2242
2243                else:
2244                    info.append("\n## Total pending limit-orders: [0]\n")
2245
2246                # --- Show stop orders section:
2247                if view["stat"]["stopOrders"]:
2248                    info.extend([
2249                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2250                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2251                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2252                    ])
2253
2254                    for item in view["stat"]["stopOrders"]:
2255                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2256                            "{} [{}]".format(item["ticker"], item["figi"]),
2257                            item["orderID"],
2258                            item["lotsRequested"],
2259                            "{} {} ({}{:.2f}%)".format(
2260                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2261                                item["baseCurrencyName"],
2262                                "+" if item["percentChanges"] > 0 else "",
2263                                float(item["percentChanges"]),
2264                            ),
2265                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2266                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2267                            item["action"],
2268                            item["type"],
2269                            item["expType"],
2270                            item["createDate"],
2271                            item["expDate"],
2272                        ))
2273
2274                else:
2275                    info.append("\n## Total stop-orders: [0]\n")
2276
2277            if details in ["full", "analytics"]:
2278                # -- Show analytics section:
2279                if view["stat"]["portfolioCostRUB"] > 0:
2280                    info.extend([
2281                        "\n# Analytics\n\n"
2282                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2283                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2284                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2285                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2286                            view["stat"]["totalChangesRUB"],
2287                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2288                            view["stat"]["totalChangesPercentRUB"],
2289                        ),
2290                        "\n## Portfolio distribution by assets\n"
2291                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2292                        "|------------------------------------|---------|---------|--------------------|\n",
2293                    ])
2294
2295                    for key in view["analytics"]["distrByAssets"].keys():
2296                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2297                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2298                                key,
2299                                view["analytics"]["distrByAssets"][key]["uniques"],
2300                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2301                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2302                            ))
2303
2304                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2305
2306                    info.extend([
2307                        "\n## Portfolio distribution by companies\n"
2308                        "\n| Company                                      | Percent | Current cost       |\n",
2309                        aSepLine,
2310                    ])
2311
2312                    for company in view["analytics"]["distrByCompanies"].keys():
2313                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2314                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2315                                "{}{}".format(
2316                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2317                                    company,
2318                                ),
2319                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2320                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2321                            ))
2322
2323                    info.extend([
2324                        "\n## Portfolio distribution by sectors\n"
2325                        "\n| Sector                                       | Percent | Current cost       |\n",
2326                        aSepLine,
2327                    ])
2328
2329                    for sector in view["analytics"]["distrBySectors"].keys():
2330                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2331                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2332                                sector,
2333                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2334                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2335                            ))
2336
2337                    info.extend([
2338                        "\n## Portfolio distribution by currencies\n"
2339                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2340                        aSepLine,
2341                    ])
2342
2343                    for curr in view["analytics"]["distrByCurrencies"].keys():
2344                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2345                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2346                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2347                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2348                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2349                            ))
2350
2351                    info.extend([
2352                        "\n## Portfolio distribution by countries\n"
2353                        "\n| Assets by country                            | Percent | Current cost       |\n",
2354                        aSepLine,
2355                    ])
2356
2357                    for country in view["analytics"]["distrByCountries"].keys():
2358                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2359                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2360                                country,
2361                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2362                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2363                            ))
2364
2365            if details in ["full", "calendar"]:
2366                # -- Show bonds payment calendar section:
2367                if view["stat"]["Bonds"]:
2368                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2369                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2370                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2371
2372                else:
2373                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2374
2375            infoText = "".join(info)
2376
2377            if show and not onlyFiles:
2378                uLogger.info(infoText)
2379
2380            if details == "full" and self.overviewFile:
2381                filename = self.overviewFile
2382
2383            elif details == "digest" and self.overviewDigestFile:
2384                filename = self.overviewDigestFile
2385
2386            elif details == "positions" and self.overviewPositionsFile:
2387                filename = self.overviewPositionsFile
2388
2389            elif details == "orders" and self.overviewOrdersFile:
2390                filename = self.overviewOrdersFile
2391
2392            elif details == "analytics" and self.overviewAnalyticsFile:
2393                filename = self.overviewAnalyticsFile
2394
2395            elif details == "calendar" and self.overviewBondsCalendarFile:
2396                filename = self.overviewBondsCalendarFile
2397
2398            else:
2399                filename = ""
2400
2401            if filename and (show or onlyFiles):
2402                with open(filename, "w", encoding="UTF-8") as fH:
2403                    fH.write(infoText)
2404
2405                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2406
2407                if self.useHTMLReports:
2408                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2409                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2410                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2411
2412                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2413
2414        return view
2415
2416    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2417        """
2418        Returns history operations between two given dates for current `accountId`.
2419        If `reportFile` string is not empty then also save human-readable report.
2420        Shows some statistical data of closed positions.
2421
2422        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2423        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2424        :param show: if `True` then also prints all records to the console.
2425        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2426        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2427        :return: original list of dictionaries with history of deals records from API ("operations" key):
2428                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2429                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2430        """
2431        if self.accountId is None or not self.accountId:
2432            uLogger.error("Variable `accountId` must be defined for using this method!")
2433            raise Exception("Account ID required")
2434
2435        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2436
2437        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2438
2439        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2440        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2441        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2442        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2443        customStat = {}  # custom statistics in additional to responseJSON
2444
2445        # --- output report in human-readable format:
2446        if show or onlyFiles or self.reportFile:
2447            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2448            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2449            nextDay = ""
2450
2451            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2452
2453            if len(ops) > 0:
2454                customStat = {
2455                    "opsCount": 0,  # total operations count
2456                    "buyCount": 0,  # buy operations
2457                    "sellCount": 0,  # sell operations
2458                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2459                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2460                    "payIn": {"rub": 0.},  # Deposit brokerage account
2461                    "payOut": {"rub": 0.},  # Withdrawals
2462                    "divs": {"rub": 0.},  # Dividends income
2463                    "coupons": {"rub": 0.},  # Coupon's income
2464                    "brokerCom": {"rub": 0.},  # Service commissions
2465                    "serviceCom": {"rub": 0.},  # Service commissions
2466                    "marginCom": {"rub": 0.},  # Margin commissions
2467                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2468                }
2469
2470                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2471                for item in ops:
2472                    if item["state"] == "OPERATION_STATE_EXECUTED":
2473                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2474
2475                        # count buy operations:
2476                        if "_BUY" in item["operationType"]:
2477                            customStat["buyCount"] += 1
2478
2479                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2480                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2484
2485                        # count sell operations:
2486                        elif "_SELL" in item["operationType"]:
2487                            customStat["sellCount"] += 1
2488
2489                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2490                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2494
2495                        # count incoming operations:
2496                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2497                            if item["payment"]["currency"] in customStat["payIn"].keys():
2498                                customStat["payIn"][item["payment"]["currency"]] += payment
2499
2500                            else:
2501                                customStat["payIn"][item["payment"]["currency"]] = payment
2502
2503                        # count withdrawals operations:
2504                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2505                            if item["payment"]["currency"] in customStat["payOut"].keys():
2506                                customStat["payOut"][item["payment"]["currency"]] += payment
2507
2508                            else:
2509                                customStat["payOut"][item["payment"]["currency"]] = payment
2510
2511                        # count dividends income:
2512                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2513                            if item["payment"]["currency"] in customStat["divs"].keys():
2514                                customStat["divs"][item["payment"]["currency"]] += payment
2515
2516                            else:
2517                                customStat["divs"][item["payment"]["currency"]] = payment
2518
2519                        # count coupon's income:
2520                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2521                            if item["payment"]["currency"] in customStat["coupons"].keys():
2522                                customStat["coupons"][item["payment"]["currency"]] += payment
2523
2524                            else:
2525                                customStat["coupons"][item["payment"]["currency"]] = payment
2526
2527                        # count broker commissions:
2528                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2529                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2530                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2531
2532                            else:
2533                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2534
2535                        # count service commissions:
2536                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2537                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2538                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2539
2540                            else:
2541                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2542
2543                        # count margin commissions:
2544                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2545                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2546                                customStat["marginCom"][item["payment"]["currency"]] += payment
2547
2548                            else:
2549                                customStat["marginCom"][item["payment"]["currency"]] = payment
2550
2551                        # count withholding taxes:
2552                        elif "_TAX" in item["operationType"]:
2553                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2554                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2555
2556                            else:
2557                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2558
2559                        else:
2560                            continue
2561
2562                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2563
2564                # --- view "Actions" lines:
2565                info.extend([
2566                    "| Report sections            |                               |                              |                      |                        |\n",
2567                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2568                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2569                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2570                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2571                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2572                    ),
2573                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2574                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2575                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2576                    ),
2577                ])
2578
2579                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2580                for key in opsKeys:
2581                    if key == "rub":
2582                        continue
2583
2584                    info.extend([
2585                        "|                            |                               | {:<28} |                      |                        |\n".format(
2586                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2587                        ),
2588                        "|                            |                               | {:<28} |                      |                        |\n".format(
2589                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2590                        ),
2591                    ])
2592
2593                info.append(splitLine1)
2594
2595                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2596                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2597                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2598                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2599                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2600                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2601                    )
2602
2603                # --- view "Payments" lines:
2604                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2605                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2606
2607                for key in paymentsKeys:
2608                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2609
2610                info.append(splitLine1)
2611
2612                # --- view "Commissions and taxes" lines:
2613                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2614                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2615
2616                for key in comKeys:
2617                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2618
2619                info.extend([
2620                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2621                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2622                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2623                ])
2624
2625            else:
2626                info.append("Broker returned no operations during this period\n")
2627
2628            # --- view "Operations" section:
2629            for item in ops:
2630                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2631                    continue
2632
2633                else:
2634                    self._figi = item["figi"]
2635                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2636                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2637
2638                    # group of deals during one day:
2639                    if nextDay and item["date"].split("T")[0] != nextDay:
2640                        info.append(splitLine2)
2641                        nextDay = ""
2642
2643                    else:
2644                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2645
2646                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2647                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2648                        self._figi if self._figi else "—",
2649                        instrument["ticker"] if instrument else "—",
2650                        instrument["type"] if instrument else "—",
2651                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2652                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2653                        TKS_OPERATION_STATES[item["state"]],
2654                        TKS_OPERATION_TYPES[item["operationType"]],
2655                    ))
2656
2657            infoText = "".join(info)
2658
2659            if show and not onlyFiles:
2660                if self.moreDebug:
2661                    uLogger.debug("Records about history of a client's operations successfully received")
2662
2663                uLogger.info(infoText)
2664
2665            if self.reportFile and (show or onlyFiles):
2666                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2667                    fH.write(infoText)
2668
2669                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2670
2671                if self.useHTMLReports:
2672                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2673                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2674                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2675
2676                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2677
2678        return ops, customStat
2679
2680    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2681        """
2682        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2683
2684        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2685        Warning! Broker server used ISO UTC time by default.
2686
2687        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2688        Also, `historyFile` used to update history with `onlyMissing` parameter.
2689
2690        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2691
2692        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2693        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2694        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2695                         `"hour"`, `"day"`. Default: `"hour"`.
2696        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2697                            False by default. Warning! History appends only from last candle to current time
2698                            with always update last candle!
2699        :param csvSep: separator if csv-file is used, `,` by default.
2700        :param show: if `True` then also prints Pandas DataFrame to the console.
2701        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2702        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2703                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2704        """
2705        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2706        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2707        history = None  # empty pandas object for history
2708
2709        if interval not in TKS_CANDLE_INTERVALS.keys():
2710            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2711            raise Exception("Incorrect value")
2712
2713        if not (self._ticker or self._figi):
2714            uLogger.error("Ticker or FIGI must be defined!")
2715            raise Exception("Ticker or FIGI required")
2716
2717        if self._ticker and not self._figi:
2718            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2719            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2720
2721        if self._figi and not self._ticker:
2722            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2723            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2724
2725        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2726        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2727        if interval.lower() != "day":
2728            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2729
2730        delta = dtEnd - dtStart  # current UTC time minus last time in file
2731        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2732
2733        # calculate history length in candles:
2734        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2735        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2736            length += 1  # to avoid fraction time
2737
2738        # calculate data blocks count:
2739        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2740
2741        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2742        if self.moreDebug:
2743            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2744            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2745            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2746            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2747
2748        tempOld = None  # pandas object for old history, if --only-missing key present
2749        lastTime = None  # datetime object of last old candle in file
2750
2751        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2752            if self.moreDebug:
2753                uLogger.debug("--only-missing key present, add only last missing candles...")
2754                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2755
2756            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2757
2758            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2759            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2760            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2761            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2762
2763            # get last datetime object from last string in file or minus 1 delta if file is empty:
2764            if len(tempOld) > 0:
2765                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2766
2767            else:
2768                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2769
2770            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2771
2772        responseJSONs = []  # raw history blocks of data
2773
2774        blockEnd = dtEnd
2775        for item in range(blocks):
2776            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2777            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2778
2779            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2780                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2781            ))
2782
2783            if blockStart == blockEnd:
2784                uLogger.debug("Skipped this zero-length block...")
2785
2786            else:
2787                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2788                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2789                self.body = str({
2790                    "figi": self._figi,
2791                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2792                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2793                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2794                })
2795                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2796
2797                if "code" in responseJSON.keys():
2798                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2799
2800                else:
2801                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2802                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2803
2804                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2805
2806            blockEnd = blockStart
2807
2808        printCount = len(responseJSONs)  # candles to show in console
2809        if responseJSONs:
2810            tempHistory = pd.DataFrame(
2811                data={
2812                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2813                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2814                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2815                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2816                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2817                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2818                    "volume": [int(item["volume"]) for item in responseJSONs],
2819                },
2820                index=range(len(responseJSONs)),
2821                columns=["date", "time", "open", "high", "low", "close", "volume"],
2822            )
2823            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2824            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2825
2826            # append only newest candles to old history if --only-missing key present:
2827            if onlyMissing and tempOld is not None and lastTime is not None:
2828                index = 0  # find start index in tempHistory data:
2829
2830                for i, item in tempHistory.iterrows():
2831                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2832
2833                    if curTime == lastTime:
2834                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2835                        index = i
2836                        printCount = index + 1
2837                        break
2838
2839                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2840
2841            else:
2842                history = tempHistory  # if no `--only-missing` key then load full data from server
2843
2844            if self.moreDebug:
2845                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2846
2847        if history is not None and not history.empty:
2848            if show and not onlyFiles:
2849                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2850                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2851                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2852                ))
2853
2854        else:
2855            uLogger.warning("Received an empty candles history!")
2856
2857        if self.historyFile is not None:
2858            if history is not None and not history.empty:
2859                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2860                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2861
2862            else:
2863                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2864
2865        else:
2866            if self.moreDebug:
2867                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2868
2869        return history
2870
2871    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2872        """
2873        Load candles history from csv-file and return Pandas DataFrame object.
2874
2875        See also: `History()` and `ShowHistoryChart()` methods.
2876
2877        :param filePath: path to csv-file to open.
2878        """
2879        loadedHistory = None  # init candles data object
2880
2881        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2882
2883        if os.path.exists(filePath):
2884            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2885
2886            tfStr = self.priceModel.FormattedDelta(
2887                self.priceModel.timeframe,
2888                "{days} days {hours}h {minutes}m {seconds}s",
2889            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2890                self.priceModel.timeframe,
2891                "{hours}h {minutes}m {seconds}s",
2892            )
2893
2894            if loadedHistory is not None and not loadedHistory.empty:
2895                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2896                    len(loadedHistory),
2897                    tfStr,
2898                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2899                )
2900
2901            else:
2902                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2903
2904        else:
2905            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2906
2907        return loadedHistory
2908
2909    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2910        """
2911        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2912
2913        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2914        Default: `index.html` (both for interact and non-interact candlesticks chart).
2915
2916        See also: `History()` and `LoadHistory()` methods.
2917
2918        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2919        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2920                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2921                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2922                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2923        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2924                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2925        """
2926        if isinstance(candles, str):
2927            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2928            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2929
2930        elif isinstance(candles, pd.DataFrame):
2931            self.priceModel.prices = candles  # set candles chain from variable
2932            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2933
2934            if "datetime" not in candles.columns:
2935                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2936
2937        else:
2938            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2939            raise Exception("Incorrect value")
2940
2941        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2942
2943        if interact:
2944            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2945
2946            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2947
2948        else:
2949            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2950
2951            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2952
2953        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2954
2955    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2956        """
2957        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2958        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2959
2960        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2961
2962        :param operation: string "Buy" or "Sell".
2963        :param lots: volume, integer count of lots >= 1.
2964        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2965        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2966        :param expDate: string "Undefined" by default or local date in future,
2967                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2968        :return: JSON with response from broker server.
2969        """
2970        if self.accountId is None or not self.accountId:
2971            uLogger.error("Variable `accountId` must be defined for using this method!")
2972            raise Exception("Account ID required")
2973
2974        if operation is None or not operation or operation not in ("Buy", "Sell"):
2975            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2976            raise Exception("Incorrect value")
2977
2978        if lots is None or lots < 1:
2979            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2980            lots = 1
2981
2982        if tp is None or tp < 0:
2983            tp = 0
2984
2985        if sl is None or sl < 0:
2986            sl = 0
2987
2988        if expDate is None or not expDate:
2989            expDate = "Undefined"
2990
2991        if not (self._ticker or self._figi):
2992            uLogger.error("Ticker or FIGI must be defined!")
2993            raise Exception("Ticker or FIGI required")
2994
2995        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2996        self._ticker = instrument["ticker"]
2997        self._figi = instrument["figi"]
2998
2999        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3000
3001        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3002        self.body = str({
3003            "figi": self._figi,
3004            "quantity": str(lots),
3005            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3006            "accountId": str(self.accountId),
3007            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3008        })
3009        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3010
3011        if "orderId" in response.keys():
3012            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3013                operation, response["orderId"],
3014                self._ticker, self._figi, lots,
3015                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3016                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3017                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3018            ))
3019
3020            if tp > 0:
3021                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3022
3023            if sl > 0:
3024                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3025
3026        else:
3027            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3028
3029        return response
3030
3031    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3032        """
3033        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3034        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3035
3036        See also: `Order()` and `Trade()` docstrings.
3037
3038        :param lots: volume, integer count of lots >= 1.
3039        :param tp: float > 0, take profit price of stop-order.
3040        :param sl: float > 0, stop loss price of stop-order.
3041        :param expDate: it's a local date in future.
3042                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3043        :return: JSON with response from broker server.
3044        """
3045        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3046
3047    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3048        """
3049        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3050        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3051
3052        See also: `Order()` and `Trade()` docstrings.
3053
3054        :param lots: volume, integer count of lots >= 1.
3055        :param tp: float > 0, take profit price of stop-order.
3056        :param sl: float > 0, stop loss price of stop-order.
3057        :param expDate: it's a local date in the future.
3058                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3059        :return: JSON with response from broker server.
3060        """
3061        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3062
3063    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3064        """
3065        Close position of given instruments.
3066
3067        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3068        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3069                         This avoids unnecessary downloading data from the server.
3070        """
3071        if instruments is None or not instruments:
3072            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3073            raise Exception("Ticker or FIGI required")
3074
3075        if isinstance(instruments, str):
3076            instruments = [instruments]
3077
3078        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3079        if uniqueInstruments:
3080            if portfolio is None or not portfolio:
3081                portfolio = self.Overview(show=False)
3082
3083            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3084            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3085
3086            for self._figi in uniqueInstruments:
3087                if self._figi not in allOpened:
3088                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3089                    continue
3090
3091                # search open trade info about instrument by ticker:
3092                instrument = {}
3093                for iType in TKS_INSTRUMENTS:
3094                    if instrument:
3095                        break
3096
3097                    for item in portfolio["stat"][iType]:
3098                        if item["figi"] == self._figi:
3099                            instrument = item
3100                            break
3101
3102                if instrument:
3103                    self._ticker = instrument["ticker"]
3104                    self._figi = instrument["figi"]
3105
3106                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3107                        self._ticker,
3108                        self._figi,
3109                        int(instrument["volume"]),
3110                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3111                    ))
3112
3113                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3114
3115                    if tradeLots > 0:
3116                        if instrument["blocked"] > 0:
3117                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3118                                instrument["blocked"],
3119                                self._ticker,
3120                                tradeLots,
3121                            ))
3122
3123                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3124                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3125
3126                    else:
3127                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3128
3129    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3130        """
3131        Close all positions of given instruments with defined type.
3132
3133        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3134        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3135                         This avoids unnecessary downloading data from the server.
3136        """
3137        if iType not in TKS_INSTRUMENTS:
3138            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3139
3140        else:
3141            if portfolio is None or not portfolio:
3142                portfolio = self.Overview(show=False)
3143
3144            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3145            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3146
3147            if tickers and portfolio:
3148                self.CloseTrades(tickers, portfolio)
3149
3150            else:
3151                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3152
3153    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3154        """
3155        Universal method to create market or limit orders with all available parameters for current `accountId`.
3156        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3157
3158        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3159        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3160
3161        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3162        then broker immediately open market order as you can do simple --buy or --sell operations!
3163
3164        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3165        When current price will go up or down to target price value then broker opens a limit order.
3166        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3167
3168        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3169
3170        :param operation: string "Buy" or "Sell".
3171        :param orderType: string "Limit" or "Stop".
3172        :param lots: volume, integer count of lots >= 1.
3173        :param targetPrice: target price > 0. This is open trade price for limit order.
3174        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3175                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3176        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3177                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3178                         Stop loss order always executed by market price.
3179        :param expDate: string "Undefined" by default or local date in future.
3180                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3181                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3182                        A limit order has no expiration date, it lasts until the end of the trading day.
3183        :return: JSON with response from broker server.
3184        """
3185        if self.accountId is None or not self.accountId:
3186            uLogger.error("Variable `accountId` must be defined for using this method!")
3187            raise Exception("Account ID required")
3188
3189        if operation is None or not operation or operation not in ("Buy", "Sell"):
3190            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3191            raise Exception("Incorrect value")
3192
3193        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3194            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3195            raise Exception("Incorrect value")
3196
3197        if lots is None or lots < 1:
3198            uLogger.error("You must define trade volume > 0: integer count of lots!")
3199            raise Exception("Incorrect value")
3200
3201        if targetPrice is None or targetPrice <= 0:
3202            uLogger.error("Target price for limit-order must be greater than 0!")
3203            raise Exception("Incorrect value")
3204
3205        if limitPrice is None or limitPrice <= 0:
3206            limitPrice = targetPrice
3207
3208        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3209            stopType = "Limit"
3210
3211        if expDate is None or not expDate:
3212            expDate = "Undefined"
3213
3214        if not (self._ticker or self._figi):
3215            uLogger.error("Tocker or FIGI must be defined!")
3216            raise Exception("Ticker or FIGI required")
3217
3218        response = {}
3219        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3220        self._ticker = instrument["ticker"]
3221        self._figi = instrument["figi"]
3222
3223        if orderType == "Limit":
3224            uLogger.debug(
3225                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3226                    self._ticker, self._figi,
3227                    operation, lots, targetPrice, instrument["currency"],
3228                ))
3229
3230            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3231            self.body = str({
3232                "figi": self._figi,
3233                "quantity": str(lots),
3234                "price": FloatToNano(targetPrice),
3235                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3236                "accountId": str(self.accountId),
3237                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3238            })
3239            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3240
3241            if "orderId" in response.keys():
3242                uLogger.info(
3243                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3244                        response["orderId"], self._ticker, self._figi, operation, lots,
3245                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3246                    ))
3247
3248                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3249                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3250                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3251                            targetPrice, instrument["currency"],
3252                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3253                        ))
3254
3255                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3256                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3257                            targetPrice, instrument["currency"],
3258                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3259                        ))
3260
3261            else:
3262                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3263
3264        if orderType == "Stop":
3265            uLogger.debug(
3266                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3267                    self._ticker, self._figi,
3268                    operation, lots,
3269                    targetPrice, instrument["currency"],
3270                    limitPrice, instrument["currency"],
3271                    stopType, expDate,
3272                ))
3273
3274            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3275            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3276            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3277
3278            body = {
3279                "figi": self._figi,
3280                "quantity": str(lots),
3281                "price": FloatToNano(limitPrice),
3282                "stopPrice": FloatToNano(targetPrice),
3283                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3284                "accountId": str(self.accountId),
3285                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3286                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3287            }
3288
3289            if expDateUTC:
3290                body["expireDate"] = expDateUTC
3291
3292            self.body = str(body)
3293            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3294
3295            if "stopOrderId" in response.keys():
3296                uLogger.info(
3297                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3298                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3299                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3300                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3301                        TKS_STOP_ORDER_TYPES[stopOrderType],
3302                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3303                    ))
3304
3305                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3306                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3307                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3308                            targetPrice, instrument["currency"],
3309                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3310                        ))
3311
3312                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3313                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3314                            targetPrice, instrument["currency"],
3315                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3316                        ))
3317
3318            else:
3319                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3320
3321        return response
3322
3323    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3324        """
3325        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3326        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3327        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3328        See also: `Order()` docstring.
3329
3330        :param lots: volume, integer count of lots >= 1.
3331        :param targetPrice: target price > 0. This is open trade price for limit order.
3332        :return: JSON with response from broker server.
3333        """
3334        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3335
3336    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3337        """
3338        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3339        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3340        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3341        target price value then broker opens a limit order. See also: `Order()` docstring.
3342
3343        :param lots: volume, integer count of lots >= 1.
3344        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3345        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3346                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3347        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3348                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3349        :param expDate: string "Undefined" by default or local date in future.
3350                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3351                        This date is converting to UTC format for server.
3352        :return: JSON with response from broker server.
3353        """
3354        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3355
3356    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3357        """
3358        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3359        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3360        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3361        See also: `Order()` docstring.
3362
3363        :param lots: volume, integer count of lots >= 1.
3364        :param targetPrice: target price > 0. This is open trade price for limit order.
3365        :return: JSON with response from broker server.
3366        """
3367        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3368
3369    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3370        """
3371        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3372        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3373        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3374        target price value then broker opens a limit order. See also: `Order()` docstring.
3375
3376        :param lots: volume, integer count of lots >= 1.
3377        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3378        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3379                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3380        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3381                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3382        :param expDate: string "Undefined" by default or local date in future.
3383                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3384                        This date is converting to UTC format for server.
3385        :return: JSON with response from broker server.
3386        """
3387        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3388
3389    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3390        """
3391        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3392
3393        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3394        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3395                             This avoids unnecessary downloading data from the server.
3396        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3397        """
3398        if self.accountId is None or not self.accountId:
3399            uLogger.error("Variable `accountId` must be defined for using this method!")
3400            raise Exception("Account ID required")
3401
3402        if orderIDs:
3403            if allOrdersIDs is None:
3404                rawOrders = self.RequestPendingOrders()
3405                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3406
3407            if allStopOrdersIDs is None:
3408                rawStopOrders = self.RequestStopOrders()
3409                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3410
3411            for orderID in orderIDs:
3412                idInPendingOrders = orderID in allOrdersIDs
3413                idInStopOrders = orderID in allStopOrdersIDs
3414
3415                if not (idInPendingOrders or idInStopOrders):
3416                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3417                    continue
3418
3419                else:
3420                    if idInPendingOrders:
3421                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3422
3423                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3424                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3425                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3426                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3427
3428                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3429                            if self.moreDebug:
3430                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3431
3432                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3433
3434                        else:
3435                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3436
3437                    elif idInStopOrders:
3438                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3439
3440                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3441                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3442                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3443                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3444
3445                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3446                            if self.moreDebug:
3447                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3448
3449                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3450
3451                        else:
3452                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3453
3454                    else:
3455                        continue
3456
3457    def CloseAllOrders(self) -> None:
3458        """
3459        Gets a list of open pending and stop orders and cancel it all.
3460        """
3461        rawOrders = self.RequestPendingOrders()
3462        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3463        lenOrders = len(allOrdersIDs)
3464
3465        rawStopOrders = self.RequestStopOrders()
3466        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3467        lenSOrders = len(allStopOrdersIDs)
3468
3469        if lenOrders > 0 or lenSOrders > 0:
3470            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3471
3472            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3473
3474        else:
3475            uLogger.info("Orders not found, nothing to cancel.")
3476
3477    def CloseAll(self, *args) -> None:
3478        """
3479        Close all available (not blocked) opened trades and orders.
3480
3481        Also, you can select one or more keywords case-insensitive:
3482        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3483
3484        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3485        """
3486        overview = self.Overview(show=False)  # get all open trades info
3487
3488        if len(args) == 0:
3489            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3490            self.CloseAllOrders()  # close all pending and stop orders
3491
3492            for iType in TKS_INSTRUMENTS:
3493                if iType != "Currencies":
3494                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3495
3496        else:
3497            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3498            lowerArgs = [x.lower() for x in args]
3499
3500            if "orders" in lowerArgs:
3501                self.CloseAllOrders()  # close all pending and stop orders
3502
3503            for iType in TKS_INSTRUMENTS:
3504                if iType.lower() in lowerArgs and iType != "Currencies":
3505                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3506
3507    def CloseAllByTicker(self, instrument: str) -> None:
3508        """
3509        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3510
3511        This method searches opened trade and orders of instrument throw all portfolio and then use
3512        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3513
3514        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3515
3516        :param instrument: string with ticker.
3517        """
3518        if instrument is None or not instrument:
3519            uLogger.error("Ticker name must be defined for using this method!")
3520            raise Exception("Ticker required")
3521
3522        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3523
3524        self._ticker = instrument  # try to set instrument as ticker
3525        self._figi = ""
3526
3527        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3528        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3529
3530        if limitAll and self.IsInLimitOrders(portfolio=overview):
3531            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3532            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3533
3534        if stopAll and self.IsInStopOrders(portfolio=overview):
3535            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3536            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3537
3538        if self.IsInPortfolio(portfolio=overview):
3539            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3540            self.CloseTrades(instruments=[instrument], portfolio=overview)
3541
3542    def CloseAllByFIGI(self, instrument: str) -> None:
3543        """
3544        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3545
3546        This method searches opened trade and orders of instrument throw all portfolio and then use
3547        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3548
3549        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3550
3551        :param instrument: string with FIGI id.
3552        """
3553        if instrument is None or not instrument:
3554            uLogger.error("FIGI id must be defined for using this method!")
3555            raise Exception("FIGI required")
3556
3557        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3558
3559        self._ticker = ""
3560        self._figi = instrument  # try to set instrument as FIGI id
3561
3562        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3563        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3564
3565        if limitAll and self.IsInLimitOrders(portfolio=overview):
3566            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3567            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3568
3569        if stopAll and self.IsInStopOrders(portfolio=overview):
3570            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3571            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3572
3573        if self.IsInPortfolio(portfolio=overview):
3574            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3575            self.CloseTrades(instruments=[instrument], portfolio=overview)
3576
3577    @staticmethod
3578    def ParseOrderParameters(operation, **inputParameters):
3579        """
3580        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3581
3582        :param operation: string "Buy" or "Sell".
3583        :param inputParameters: this is dict of strings that looks like this
3584               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3585               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3586               "prices" key: one or more prices to open limit-orders
3587               Counts of values in lots and prices lists must be equals!
3588        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3589        """
3590        # TODO: update order grid work with api v2
3591        pass
3592        # uLogger.debug("Input parameters: {}".format(inputParameters))
3593        #
3594        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3595        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3596        #     raise Exception("Incorrect value")
3597        #
3598        # if "l" in inputParameters.keys():
3599        #     inputParameters["lots"] = inputParameters.pop("l")
3600        #
3601        # if "p" in inputParameters.keys():
3602        #     inputParameters["prices"] = inputParameters.pop("p")
3603        #
3604        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3605        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3606        #     raise Exception("Incorrect value")
3607        #
3608        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3609        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3610        #
3611        # if len(lots) != len(prices):
3612        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3613        #     raise Exception("Incorrect value")
3614        #
3615        # uLogger.debug("Extracted parameters for orders:")
3616        # uLogger.debug("lots = {}".format(lots))
3617        # uLogger.debug("prices = {}".format(prices))
3618        #
3619        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3620        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3621        # uLogger.debug("Order parameters: {}".format(result))
3622        #
3623        # return result
3624
3625    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3626        """
3627        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3628
3629        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3630        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3631        """
3632        result = False
3633        msg = "Instrument not defined!"
3634
3635        if portfolio is None or not portfolio:
3636            portfolio = self.Overview(show=False)
3637
3638        if self._ticker:
3639            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3640            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3641
3642            for iType in TKS_INSTRUMENTS:
3643                for instrument in portfolio["stat"][iType]:
3644                    if instrument["ticker"] == self._ticker:
3645                        result = True
3646                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3647                        break
3648
3649        elif self._figi:
3650            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3651            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3652
3653            for iType in TKS_INSTRUMENTS:
3654                for instrument in portfolio["stat"][iType]:
3655                    if instrument["figi"] == self._figi:
3656                        result = True
3657                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3658                        break
3659
3660        else:
3661            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3662
3663        uLogger.debug(msg)
3664
3665        return result
3666
3667    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3668        """
3669        Returns instrument from the user's portfolio if it presents there.
3670        Instrument must be defined by `ticker` (highly priority) or `figi`.
3671
3672        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3673        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3674        """
3675        result = None
3676        msg = "Instrument not defined!"
3677
3678        if portfolio is None or not portfolio:
3679            portfolio = self.Overview(show=False)
3680
3681        if self._ticker:
3682            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3683            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3684
3685            for iType in TKS_INSTRUMENTS:
3686                for instrument in portfolio["stat"][iType]:
3687                    if instrument["ticker"] == self._ticker:
3688                        result = instrument
3689                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3690                        break
3691
3692        elif self._figi:
3693            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3694            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3695
3696            for iType in TKS_INSTRUMENTS:
3697                for instrument in portfolio["stat"][iType]:
3698                    if instrument["figi"] == self._figi:
3699                        result = instrument
3700                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3701                        break
3702
3703        else:
3704            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3705
3706        uLogger.debug(msg)
3707
3708        return result
3709
3710    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3711        """
3712        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3713
3714        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3715
3716        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3717        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3718        """
3719        result = False
3720        msg = "Instrument not defined!"
3721
3722        if portfolio is None or not portfolio:
3723            portfolio = self.Overview(show=False)
3724
3725        if self._ticker:
3726            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3727            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3728
3729            for instrument in portfolio["stat"]["orders"]:
3730                if instrument["ticker"] == self._ticker:
3731                    result = True
3732                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3733                    break
3734
3735        elif self._figi:
3736            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3737            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3738
3739            for instrument in portfolio["stat"]["orders"]:
3740                if instrument["figi"] == self._figi:
3741                    result = True
3742                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3743                    break
3744
3745        else:
3746            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3747
3748        uLogger.debug(msg)
3749
3750        return result
3751
3752    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3753        """
3754        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3755        Instrument must be defined by `ticker` (highly priority) or `figi`.
3756
3757        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3758
3759        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3760        :return: list with `orderID`s of limit orders.
3761        """
3762        result = []
3763        msg = "Instrument not defined!"
3764
3765        if portfolio is None or not portfolio:
3766            portfolio = self.Overview(show=False)
3767
3768        if self._ticker:
3769            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3770            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3771
3772            for instrument in portfolio["stat"]["orders"]:
3773                if instrument["ticker"] == self._ticker:
3774                    result.append(instrument["orderID"])
3775
3776            if result:
3777                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3778
3779        elif self._figi:
3780            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3781            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3782
3783            for instrument in portfolio["stat"]["orders"]:
3784                if instrument["figi"] == self._figi:
3785                    result.append(instrument["orderID"])
3786
3787            if result:
3788                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3789
3790        else:
3791            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3792
3793        uLogger.debug(msg)
3794
3795        return result
3796
3797    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3798        """
3799        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3800
3801        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3802
3803        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3804        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3805        """
3806        result = False
3807        msg = "Instrument not defined!"
3808
3809        if portfolio is None or not portfolio:
3810            portfolio = self.Overview(show=False)
3811
3812        if self._ticker:
3813            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3814            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3815
3816            for instrument in portfolio["stat"]["stopOrders"]:
3817                if instrument["ticker"] == self._ticker:
3818                    result = True
3819                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3820                    break
3821
3822        elif self._figi:
3823            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3824            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3825
3826            for instrument in portfolio["stat"]["stopOrders"]:
3827                if instrument["figi"] == self._figi:
3828                    result = True
3829                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3830                    break
3831
3832        else:
3833            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3834
3835        uLogger.debug(msg)
3836
3837        return result
3838
3839    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3840        """
3841        Returns list with all `orderID`s of opened stop orders for the instrument.
3842        Instrument must be defined by `ticker` (highly priority) or `figi`.
3843
3844        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3845
3846        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3847        :return: list with `orderID`s of stop orders.
3848        """
3849        result = []
3850        msg = "Instrument not defined!"
3851
3852        if portfolio is None or not portfolio:
3853            portfolio = self.Overview(show=False)
3854
3855        if self._ticker:
3856            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3857            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3858
3859            for instrument in portfolio["stat"]["stopOrders"]:
3860                if instrument["ticker"] == self._ticker:
3861                    result.append(instrument["orderID"])
3862
3863            if result:
3864                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3865
3866        elif self._figi:
3867            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3868            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3869
3870            for instrument in portfolio["stat"]["stopOrders"]:
3871                if instrument["figi"] == self._figi:
3872                    result.append(instrument["orderID"])
3873
3874            if result:
3875                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3876
3877        else:
3878            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3879
3880        uLogger.debug(msg)
3881
3882        return result
3883
3884    def RequestLimits(self) -> dict:
3885        """
3886        Method for obtaining the available funds for withdrawal for current `accountId`.
3887
3888        See also:
3889        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3890        - `OverviewLimits()` method
3891
3892        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3893                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3894                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3895                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3896        """
3897        if self.accountId is None or not self.accountId:
3898            uLogger.error("Variable `accountId` must be defined for using this method!")
3899            raise Exception("Account ID required")
3900
3901        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3902
3903        self.body = str({"accountId": self.accountId})
3904        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3905        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3906
3907        if self.moreDebug:
3908            uLogger.debug("Records about available funds for withdrawal successfully received")
3909
3910        return rawLimits
3911
3912    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3913        """
3914        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3915
3916        See also: `RequestLimits()`.
3917
3918        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3919        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3920        :return: dict with raw parsed data from server and some calculated statistics about it.
3921        """
3922        if self.accountId is None or not self.accountId:
3923            uLogger.error("Variable `accountId` must be defined for using this method!")
3924            raise Exception("Account ID required")
3925
3926        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3927
3928        view = {
3929            "rawLimits": rawLimits,
3930            "limits": {  # parsed data for every currency:
3931                "money": {  # this is an array of portfolio currency positions
3932                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3933                },
3934                "blocked": {  # this is an array of blocked currency
3935                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3936                },
3937                "blockedGuarantee": {  # this is locked money under collateral for futures
3938                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3939                },
3940            },
3941        }
3942
3943        # --- Prepare text table with limits in human-readable format:
3944        if show or onlyFiles:
3945            info = [
3946                "# Withdrawal limits\n\n",
3947                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3948                "* **Account ID:** [{}]\n".format(self.accountId),
3949            ]
3950
3951            if view["limits"]["money"]:
3952                info.extend([
3953                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3954                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3955                ])
3956
3957            else:
3958                info.append("\nNo withdrawal limits\n")
3959
3960            for curr in view["limits"]["money"].keys():
3961                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3962                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3963                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3964
3965                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3966                    "[{}]".format(curr),
3967                    "{:.2f}".format(view["limits"]["money"][curr]),
3968                    "{:.2f}".format(availableMoney),
3969                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3970                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3971                )
3972
3973                if curr == "rub":
3974                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3975
3976                else:
3977                    info.append(infoStr)
3978
3979            infoText = "".join(info)
3980
3981            if show and not onlyFiles:
3982                uLogger.info(infoText)
3983
3984            if self.withdrawalLimitsFile and (show or onlyFiles):
3985                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3986                    fH.write(infoText)
3987
3988                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3989
3990                if self.useHTMLReports:
3991                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3992                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3993                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3994
3995                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3996
3997        return view
3998
3999    def RequestAccounts(self) -> dict:
4000        """
4001        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4002
4003        See also:
4004        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4005        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4006        - `OverviewUserInfo()` method
4007
4008        :return: dict with raw data from server that contains accounts info. Example of dict:
4009                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4010                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4011                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4012                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4013        """
4014        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4015
4016        self.body = str({})
4017        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4018        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4019
4020        if self.moreDebug:
4021            uLogger.debug("Records about available accounts successfully received")
4022
4023        return rawAccounts
4024
4025    def RequestUserInfo(self) -> dict:
4026        """
4027        Method for requesting common user's information.
4028
4029        See also:
4030        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4031        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4032        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4033        - `OverviewUserInfo()` method
4034
4035        :return: dict with raw data from server that contains user's information. Example of dict:
4036                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4037                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4038        """
4039        uLogger.debug("Requesting common user's information. Wait, please...")
4040
4041        self.body = str({})
4042        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4043        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4044
4045        if self.moreDebug:
4046            uLogger.debug("Records about current user successfully received")
4047
4048        return rawUserInfo
4049
4050    def RequestMarginStatus(self, accountId: str = None) -> dict:
4051        """
4052        Method for requesting margin calculation for defined account ID.
4053
4054        See also:
4055        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4056        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4057        - `OverviewUserInfo()` method
4058
4059        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4060        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4061                 Example of responses:
4062                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4063                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4064                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4065                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4066                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4067                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4068        """
4069        if accountId is None or not accountId:
4070            if self.accountId is None or not self.accountId:
4071                uLogger.error("Variable `accountId` must be defined for using this method!")
4072                raise Exception("Account ID required")
4073
4074            else:
4075                accountId = self.accountId  # use `self.accountId` (main ID) by default
4076
4077        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4078
4079        self.body = str({"accountId": accountId})
4080        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4081        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4082
4083        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4084            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4085            rawMargin = {}
4086
4087        else:
4088            if self.moreDebug:
4089                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4090
4091        return rawMargin
4092
4093    def RequestTariffLimits(self) -> dict:
4094        """
4095        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4096
4097        See also:
4098        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4099        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4100        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4101        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4102        - `OverviewUserInfo()` method
4103
4104        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4105                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4106                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4107        """
4108        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4109
4110        self.body = str({})
4111        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4112        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4113
4114        if self.moreDebug:
4115            uLogger.debug("Records with limits of current tariff successfully received")
4116
4117        return rawTariffLimits
4118
4119    def RequestBondCoupons(self, iJSON: dict) -> dict:
4120        """
4121        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4122        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4123        All dates are in UTC timezone.
4124
4125        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4126        Documentation:
4127        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4128        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4129
4130        See also: `ExtendBondsData()`.
4131
4132        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4133                      If raw iJSON is not data of bond then server returns an error [400] with message:
4134                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4135        :return: dictionary with bond payment calendar. Response example
4136                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4137                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4138                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4139                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4140        """
4141        if iJSON["figi"] is None or not iJSON["figi"]:
4142            uLogger.error("FIGI must be defined for using this method!")
4143            raise Exception("FIGI required")
4144
4145        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4146        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4147
4148        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4149            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4150            self._figi,
4151            startDate,
4152            endDate,
4153        ))
4154
4155        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4156        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4157        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4158
4159        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4160            uLogger.warning("Instrument type is not bond!")
4161
4162        else:
4163            if self.moreDebug:
4164                uLogger.debug("Records about bond payment calendar successfully received")
4165
4166        return calendar
4167
4168    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4169        """
4170        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4171        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4172        coupon yields, current yields and some statistics etc.
4173
4174        WARNING! This is too long operation if a lot of bonds requested from broker server.
4175
4176        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4177
4178        :param instruments: list of strings with tickers or FIGIs.
4179        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4180                     for further used by data scientists or stock analytics.
4181        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4182                 In XLSX-file and Pandas DataFrame fields mean:
4183                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4184                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4185        """
4186        if instruments is None or not instruments:
4187            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4188            raise Exception("Ticker or FIGI required")
4189
4190        if isinstance(instruments, str):
4191            instruments = [instruments]
4192
4193        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4194
4195        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4196
4197        iCount = len(uniqueInstruments)
4198        tooLong = iCount >= 20
4199        if tooLong:
4200            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4201
4202        bonds = None
4203        for i, self._figi in enumerate(uniqueInstruments):
4204            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4205
4206            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4207                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4208                rawBond = self.SearchByFIGI(requestPrice=True)
4209
4210                # Widen raw data with UTC current time (iData["actualDateTime"]):
4211                actualDate = datetime.now(tzutc())
4212                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4213
4214                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4215                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4216
4217                # Replace some values with human-readable:
4218                iData["nominalCurrency"] = iData["nominal"]["currency"]
4219                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4220                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4221                iData["aciCurrency"] = iData["aciValue"]["currency"]
4222                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4223                iData["issueSize"] = int(iData["issueSize"])
4224                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4225                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4226                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4227                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4228                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4229                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4230                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4231                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4232                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4233                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4234
4235                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4236                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4237                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4238                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4239                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4240                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4241                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4242                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4243                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4244                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4245                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4246
4247                # Widen raw data with calendar data from `rawCalendar` values:
4248                calendarData = []
4249                if "events" in iData["rawCalendar"].keys():
4250                    for item in iData["rawCalendar"]["events"]:
4251                        calendarData.append({
4252                            "couponDate": item["couponDate"],
4253                            "couponNumber": int(item["couponNumber"]),
4254                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4255                            "payCurrency": item["payOneBond"]["currency"],
4256                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4257                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4258                            "couponStartDate": item["couponStartDate"],
4259                            "couponEndDate": item["couponEndDate"],
4260                            "couponPeriod": item["couponPeriod"],
4261                        })
4262
4263                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4264                    if "maturityDate" not in iData.keys():
4265                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4266
4267                # Widen raw data with Coupon Rate.
4268                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4269                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4270                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4271                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4272
4273                # Widen raw data with Yield to Maturity (YTM) on current date.
4274                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4275                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4276                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4277                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4278                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4279                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4280
4281                iData["calendar"] = calendarData  # adds calendar at the end
4282
4283                # Remove not used data:
4284                iData.pop("uid")
4285                iData.pop("positionUid")
4286                iData.pop("currentPrice")
4287                iData.pop("rawCalendar")
4288
4289                colNames = list(iData.keys())
4290                if bonds is None:
4291                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4292
4293                else:
4294                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4295
4296            else:
4297                uLogger.warning("Instrument is not a bond!")
4298
4299            processed = round(100 * (i + 1) / iCount, 1)
4300            if tooLong and processed % 5 == 0:
4301                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4302
4303            else:
4304                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4305
4306        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4307
4308        # Saving bonds from Pandas DataFrame to XLSX sheet:
4309        if xlsx and self.bondsXLSXFile:
4310            with pd.ExcelWriter(
4311                    path=self.bondsXLSXFile,
4312                    date_format=TKS_DATE_FORMAT,
4313                    datetime_format=TKS_DATE_TIME_FORMAT,
4314                    mode="w",
4315            ) as writer:
4316                bonds.to_excel(
4317                    writer,
4318                    sheet_name="Extended bonds data",
4319                    index=True,
4320                    encoding="UTF-8",
4321                    freeze_panes=(1, 1),
4322                )  # saving as XLSX-file with freeze first row and column as headers
4323
4324            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4325
4326        return bonds
4327
4328    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4329        """
4330        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4331
4332        WARNING! This is too long operation if a lot of bonds requested from broker server.
4333
4334        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4335
4336        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4337                        extended information about bonds: main info, current prices, bond payment calendar,
4338                        coupon yields, current yields and some statistics etc.
4339                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4340        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4341                     for further used by data scientists or stock analytics.
4342        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4343        """
4344        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4345            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4346
4347        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4348
4349        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4350        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4351        calendar = None
4352        for bond in extBonds.iterrows():
4353            for item in bond[1]["calendar"]:
4354                cData = {
4355                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4356                    "couponDate": item["couponDate"],
4357                    "figi": bond[1]["figi"],
4358                    "ticker": bond[1]["ticker"],
4359                    "name": bond[1]["name"],
4360                    "couponNumber": item["couponNumber"],
4361                    "payOneBond": item["payOneBond"],
4362                    "payCurrency": item["payCurrency"],
4363                    "couponType": item["couponType"],
4364                    "couponPeriod": item["couponPeriod"],
4365                    "fixDate": item["fixDate"],
4366                    "couponStartDate": item["couponStartDate"],
4367                    "couponEndDate": item["couponEndDate"],
4368                }
4369
4370                if calendar is None:
4371                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4372
4373                else:
4374                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4375
4376        if calendar is not None:
4377            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4378
4379            # Saving calendar from Pandas DataFrame to XLSX sheet:
4380            if xlsx:
4381                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4382
4383                with pd.ExcelWriter(
4384                        path=xlsxCalendarFile,
4385                        date_format=TKS_DATE_FORMAT,
4386                        datetime_format=TKS_DATE_TIME_FORMAT,
4387                        mode="w",
4388                ) as writer:
4389                    humanReadable = calendar.copy(deep=True)
4390                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4391                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4392                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4393                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4394                    humanReadable.columns = colNames  # human-readable column names
4395
4396                    humanReadable.to_excel(
4397                        writer,
4398                        sheet_name="Bond payments calendar",
4399                        index=False,
4400                        encoding="UTF-8",
4401                        freeze_panes=(1, 2),
4402                    )  # saving as XLSX-file with freeze first row and column as headers
4403
4404                    del humanReadable  # release df in memory
4405
4406                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4407
4408        return calendar
4409
4410    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4411        """
4412        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4413        Also, creates Markdown file with calendar data, `calendar.md` by default.
4414
4415        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4416
4417        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4418                        extended information about bonds: main info, current prices, bond payment calendar,
4419                        coupon yields, current yields and some statistics etc.
4420                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4421        :param show: if `True` then also printing bonds payment calendar to the console,
4422                     otherwise save to file `calendarFile` only. `False` by default.
4423        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4424        :return: multilines text in Markdown format with bonds payment calendar as a table.
4425        """
4426        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4427            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4428
4429        infoText = "# Bond payments calendar\n\n"
4430
4431        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4432
4433        if not (calendar is None or calendar.empty):
4434            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4435
4436            info = [
4437                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4438                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4439                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4440            ]
4441
4442            newMonth = False
4443            notOneBond = calendar["figi"].nunique() > 1
4444            for i, bond in enumerate(calendar.iterrows()):
4445                if newMonth and notOneBond:
4446                    info.append(splitLine)
4447
4448                info.append(
4449                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4450                        "  √" if bond[1]["paid"] else "  —",
4451                        bond[1]["couponDate"].split("T")[0],
4452                        bond[1]["figi"],
4453                        bond[1]["ticker"],
4454                        bond[1]["couponNumber"],
4455                        "{} {}".format(
4456                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4457                            bond[1]["payCurrency"],
4458                        ),
4459                        bond[1]["couponType"],
4460                        bond[1]["couponPeriod"],
4461                        bond[1]["fixDate"].split("T")[0],
4462                    )
4463                )
4464
4465                if i < len(calendar.values) - 1:
4466                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4467                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4468                    newMonth = False if curDate.month == nextDate.month else True
4469
4470                else:
4471                    newMonth = False
4472
4473            infoText += "".join(info)
4474
4475            if show and not onlyFiles:
4476                uLogger.info("{}".format(infoText))
4477
4478            if self.calendarFile is not None and (show or onlyFiles):
4479                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4480                    fH.write(infoText)
4481
4482                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4483
4484                if self.useHTMLReports:
4485                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4486                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4487                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4488
4489                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4490
4491        else:
4492            infoText += "No data\n"
4493
4494        return infoText
4495
4496    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4497        """
4498        Method for parsing and show simple table with all available user accounts.
4499
4500        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4501
4502        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4503        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4504        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4505                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4506                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4507                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4508                                                        "closed": "—", "access": "Full access" }, ...}}`
4509        """
4510        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4511
4512        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4513        accounts = {
4514            item["id"]: {
4515                "type": TKS_ACCOUNT_TYPES[item["type"]],
4516                "name": item["name"],
4517                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4518                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4519                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4520                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4521            } for item in rawAccounts["accounts"]
4522        }
4523
4524        # Raw and parsed data with some fields replaced in "stat" section:
4525        view = {
4526            "rawAccounts": rawAccounts,
4527            "stat": accounts,
4528        }
4529
4530        # --- Prepare simple text table with only accounts data in human-readable format:
4531        if show or onlyFiles:
4532            info = [
4533                "# User accounts\n\n",
4534                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4535                "| Account ID   | Type                      | Status                    | Name                           |\n",
4536                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4537            ]
4538
4539            for account in view["stat"].keys():
4540                info.extend([
4541                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4542                        account,
4543                        view["stat"][account]["type"],
4544                        view["stat"][account]["status"],
4545                        view["stat"][account]["name"],
4546                    )
4547                ])
4548
4549            infoText = "".join(info)
4550
4551            if show and not onlyFiles:
4552                uLogger.info(infoText)
4553
4554            if self.userAccountsFile and (show or onlyFiles):
4555                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4556                    fH.write(infoText)
4557
4558                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4559
4560                if self.useHTMLReports:
4561                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4562                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4563                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4564
4565                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4566
4567        return view
4568
4569    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4570        """
4571        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4572
4573        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4574
4575        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4576        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4577        :return: dict with raw parsed data from server and some calculated statistics about it.
4578        """
4579        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4580        tmpTicker = self._ticker
4581        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4582        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4583        self._ticker = tmpTicker
4584
4585        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4586        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4587        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4588        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4589        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4590        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4591
4592        # This is dict with parsed common user data:
4593        userInfo = {
4594            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4595            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4596            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4597            "tariff": rawUserInfo["tariff"],
4598        }
4599
4600        # This is an array of dict with parsed margin statuses for every account IDs:
4601        margins = {}
4602        for accountId in accounts.keys():
4603            if rawMargins[accountId]:
4604                margins[accountId] = {
4605                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4606                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4607                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4608                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4609                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4610                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4611                    "missing": missing["volume"],
4612                }
4613
4614            else:
4615                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4616
4617        unary = {}  # unary-connection limits
4618        for item in rawTariffLimits["unaryLimits"]:
4619            if item["limitPerMinute"] in unary.keys():
4620                unary[item["limitPerMinute"]].extend(item["methods"])
4621
4622            else:
4623                unary[item["limitPerMinute"]] = item["methods"]
4624
4625        stream = {}  # stream-connection limits
4626        for item in rawTariffLimits["streamLimits"]:
4627            if item["limit"] in stream.keys():
4628                stream[item["limit"]].extend(item["streams"])
4629
4630            else:
4631                stream[item["limit"]] = item["streams"]
4632
4633        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4634        limits = {
4635            "unary": unary,
4636            "stream": stream,
4637        }
4638
4639        # Raw and parsed data as an output result:
4640        view = {
4641            "rawUserInfo": rawUserInfo,
4642            "rawAccounts": rawAccounts,
4643            "rawMargins": rawMargins,
4644            "rawTariffLimits": rawTariffLimits,
4645            "stat": {
4646                "overview": overview,
4647                "userInfo": userInfo,
4648                "accounts": accounts,
4649                "margins": margins,
4650                "limits": limits,
4651            },
4652        }
4653
4654        # --- Prepare text table with user information in human-readable format:
4655        if show or onlyFiles:
4656            info = [
4657                "# Full user information\n\n",
4658                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4659                "## Common information\n\n",
4660                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4661                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4662                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4663                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4664                "\n## User accounts\n\n",
4665            ]
4666
4667            for account in view["stat"]["accounts"].keys():
4668                info.extend([
4669                    "### ID: [{}]\n\n".format(account),
4670                    "| Parameters           | Values                                                       |\n",
4671                    "|----------------------|--------------------------------------------------------------|\n",
4672                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4673                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4674                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4675                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4676                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4677                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4678                ])
4679
4680                if margins[account]:
4681                    info.extend([
4682                        "| Margin status:       | Enabled                                                      |\n",
4683                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4684                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4685                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4686                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4687                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4688                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4689                    ])
4690
4691                else:
4692                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4693
4694            info.extend([
4695                "\n## Current user tariff limits\n",
4696                "\n### See also\n",
4697                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4698                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4699                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4700                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4701                "\n### Unary limits\n",
4702            ])
4703
4704            if unary:
4705                for key, values in sorted(unary.items()):
4706                    info.append("\n* Max requests per minute: {}\n".format(key))
4707
4708                    for value in values:
4709                        info.append("  - {}\n".format(value))
4710
4711            else:
4712                info.append("\nNot available\n")
4713
4714            info.append("\n### Stream limits\n")
4715
4716            if stream:
4717                for key, values in sorted(stream.items()):
4718                    info.append("\n* Max stream connections: {}\n".format(key))
4719
4720                    for value in values:
4721                        info.append("  - {}\n".format(value))
4722
4723            else:
4724                info.append("\nNot available\n")
4725
4726            infoText = "".join(info)
4727
4728            if show and not onlyFiles:
4729                uLogger.info(infoText)
4730
4731            if self.userInfoFile and (show or onlyFiles):
4732                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4733                    fH.write(infoText)
4734
4735                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4736
4737                if self.useHTMLReports:
4738                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4739                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4740                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4741
4742                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4743
4744        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 87        """
 88        Main class init.
 89
 90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 93        :param useCache: use default cache file with raw data to use instead of `iList`.
 94                         True by default. Cache is auto-update if new day has come.
 95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 96        :param defaultCache: path to default cache file. `dump.json` by default.
 97        """
 98        if token is None or not token:
 99            try:
100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
102
103            except KeyError:
104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
105                raise Exception("Token required")
106
107        else:
108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
110
111        if accountId is None or not accountId:
112            try:
113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
115
116            except KeyError:
117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
118
119        else:
120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
122
123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
125
126        Latest version: https://pypi.org/project/tksbrokerapi/
127        """
128
129        self.__lock = Lock()  # initialize multiprocessing mutex lock
130
131        self.aliases = TKS_TICKER_ALIASES
132        """Some aliases instead official tickers.
133
134        See also: `TKSEnums.TKS_TICKER_ALIASES`
135        """
136
137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
138
139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
140
141        self._ticker = ""
142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
143
144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
146
147        See also: `SearchByTicker()`, `SearchInstruments()`.
148        """
149
150        self._figi = ""
151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
152
153        See also: `SearchByFIGI()`, `SearchInstruments()`.
154        """
155
156        self.depth = 1
157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
158
159        See also: `GetCurrentPrices()`.
160        """
161
162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
164
165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
166        """
167
168        uLogger.debug("Broker API server: {}".format(self.server))
169
170        self.timeout = 15
171        """Server operations timeout in seconds. Default: `15`.
172
173        See also: `SendAPIRequest()`.
174        """
175
176        self.headers = {
177            "Content-Type": "application/json",
178            "accept": "application/json",
179            "Authorization": "Bearer {}".format(self.token),
180            "x-app-name": "Tim55667757.TKSBrokerAPI",
181        }
182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
183
184        See also: `SendAPIRequest()`.
185        """
186
187        self.body = None
188        """Request body which send to broker server. Default: `None`.
189
190        See also: `SendAPIRequest()`.
191        """
192
193        self.moreDebug = False
194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
195
196        self.useHTMLReports = False
197        """
198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
199        
200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
201        """
202
203        self.historyFile = None
204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
205
206        See also: `History()`.
207        """
208
209        self.htmlHistoryFile = "index.html"
210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
211
212        See also: `ShowHistoryChart()`.
213        """
214
215        self.instrumentsFile = "instruments.md"
216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
217
218        See also: `ShowInstrumentsInfo()`.
219        """
220
221        self.searchResultsFile = "search-results.md"
222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
223
224        See also: `SearchInstruments()`.
225        """
226
227        self.pricesFile = "prices.md"
228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
229
230        See also: `GetListOfPrices()`.
231        """
232
233        self.infoFile = "info.md"
234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
235
236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
237        """
238
239        self.bondsXLSXFile = "ext-bonds.xlsx"
240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
242
243        See also: `ExtendBondsData()`.
244        """
245
246        self.calendarFile = "calendar.md"
247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
248        
249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
250
251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
252        """
253
254        self.overviewFile = "overview.md"
255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
256
257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
258        """
259
260        self.overviewDigestFile = "overview-digest.md"
261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
262
263        See also: `Overview()` with parameter `details="digest"`.
264        """
265
266        self.overviewPositionsFile = "overview-positions.md"
267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
268
269        See also: `Overview()` with parameter `details="positions"`.
270        """
271
272        self.overviewOrdersFile = "overview-orders.md"
273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
274
275        See also: `Overview()` with parameter `details="orders"`.
276        """
277
278        self.overviewAnalyticsFile = "overview-analytics.md"
279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
280
281        See also: `Overview()` with parameter `details="analytics"`.
282        """
283
284        self.overviewBondsCalendarFile = "overview-calendar.md"
285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
286
287        See also: `Overview()` with parameter `details="calendar"`.
288        """
289
290        self.reportFile = "deals.md"
291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
292
293        See also: `Deals()`.
294        """
295
296        self.withdrawalLimitsFile = "limits.md"
297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
298
299        See also: `OverviewLimits()` and `RequestLimits()`.
300        """
301
302        self.userInfoFile = "user-info.md"
303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
304
305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
306        """
307
308        self.userAccountsFile = "accounts.md"
309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
310
311        See also: `OverviewAccounts()`, `RequestAccounts()`.
312        """
313
314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
316
317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
318
319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
320        """
321
322        self.iList = None  # init iList for raw instruments data
323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
324        
325        See also: `Listing()`, `DumpInstruments()`.
326        """
327
328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
329        if useCache:
330            if os.path.exists(self.iListDumpFile):
331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
332                curTime = datetime.now(tzutc())
333
334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
336
337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
338
339                else:
340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
341
342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
343                        os.path.abspath(self.iListDumpFile),
344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
345                    ))
346
347            else:
348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
350
351        else:
352            self.iList = self.Listing()  # request new raw instruments data from broker server
353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
354
355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
357
358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
359        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
413    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
414        """
415        Send GET or POST request to broker server and receive JSON object.
416
417        self.header: must be defining with dictionary of headers.
418        self.body: if define then used as request body. None by default.
419        self.timeout: global request timeout, 15 seconds by default.
420        :param url: url with REST request.
421        :param reqType: send "GET" or "POST" request. "GET" by default.
422        :param retry: how many times retry after first request if an 5xx server errors occurred.
423        :param pause: sleep time in seconds between retries.
424        :return: response JSON (dictionary) from broker.
425        """
426        if reqType.upper() not in ("GET", "POST"):
427            uLogger.error("You can define request type: `GET` or `POST`!")
428            raise Exception("Incorrect value")
429
430        if self.moreDebug:
431            uLogger.debug("Request parameters:")
432            uLogger.debug("    - REST API URL: {}".format(url))
433            uLogger.debug("    - request type: {}".format(reqType))
434            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
435            uLogger.debug("    - body:\n{}".format(self.body))
436
437        # fast hack to avoid all operations with some tickers/FIGI
438        responseJSON = {}
439        oK = True
440        for item in self.exclude:
441            if item in url:
442                if self.moreDebug:
443                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
444
445                oK = False
446                break
447
448        if oK:
449            with self.__lock:  # acquire the mutex lock
450                counter = 0
451                response = None
452                errMsg = ""
453
454                while not response and counter <= retry:
455                    if reqType == "GET":
456                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
457
458                    if reqType == "POST":
459                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
460
461                    if self.moreDebug:
462                        uLogger.debug("Response:")
463                        uLogger.debug("    - status code: {}".format(response.status_code))
464                        uLogger.debug("    - reason: {}".format(response.reason))
465                        uLogger.debug("    - body length: {}".format(len(response.text)))
466                        uLogger.debug("    - headers:\n{}".format(response.headers))
467
468                    # Server returns some headers:
469                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
470                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
471                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
472                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
473                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
474                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
475                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
476                        sleep(rateLimitWait)
477
478                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
479                    if 400 <= response.status_code < 500:
480                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
481                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
482
483                        if "code" in response.text and "message" in response.text:
484                            msgDict = self._ParseJSON(rawData=response.text)
485                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
486
487                        counter = retry + 1  # do not retry for 4xx errors
488
489                    if 500 <= response.status_code < 600:
490                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
491                        uLogger.debug("    - not oK, {}".format(errMsg))
492
493                        if "code" in response.text and "message" in response.text:
494                            errMsgDict = self._ParseJSON(rawData=response.text)
495                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
496
497                        counter += 1
498
499                        if counter <= retry:
500                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
501                            sleep(pause)
502
503                responseJSON = self._ParseJSON(rawData=response.text)
504
505                if errMsg:
506                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
507                    uLogger.error("    - not oK, {}".format(errMsg))
508
509        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
542    def Listing(self) -> dict:
543        """
544        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
545
546        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
547        """
548        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
549        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
550
551        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
552        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
553        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
554
555        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
556        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
557        poolUpdater.close()  # close the thread pool
558        poolUpdater.join()  # wait a moment until all data returns from threads
559
560        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
561        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
562        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
563
564        # calculate minimum price increment (step) for all instruments and set up instrument's type:
565        for iType in iList.keys():
566            for ticker in iList[iType]:
567                iList[iType][ticker]["type"] = iType
568
569                if "minPriceIncrement" in iList[iType][ticker].keys():
570                    iList[iType][ticker]["step"] = NanoToFloat(
571                        iList[iType][ticker]["minPriceIncrement"]["units"],
572                        iList[iType][ticker]["minPriceIncrement"]["nano"],
573                    )
574
575                else:
576                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
577
578        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
580    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
581        """
582        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
583
584        See also: `DumpInstruments()`, `Listing()`.
585
586        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
587                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
588        """
589        if self.iListDumpFile is None or not self.iListDumpFile:
590            uLogger.error("Output name of dump file must be defined!")
591            raise Exception("Filename required")
592
593        if not self.iList or forceUpdate:
594            self.iList = self.Listing()
595
596        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
597
598        # Save as XLSX with separated sheets for every type of instruments:
599        with pd.ExcelWriter(
600                path=xlsxDumpFile,
601                date_format=TKS_DATE_FORMAT,
602                datetime_format=TKS_DATE_TIME_FORMAT,
603                mode="w",
604        ) as writer:
605            for iType in TKS_INSTRUMENTS:
606                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
607                df = df[sorted(df)]  # sorted by column names
608                df = df.applymap(
609                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
610                    na_action="ignore",
611                )  # converting numbers from nano-type to float in every cell
612                df.to_excel(
613                    writer,
614                    sheet_name=iType,
615                    encoding="UTF-8",
616                    freeze_panes=(1, 1),
617                )  # saving as XLSX-file with freeze first row and column as headers
618
619        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
621    def DumpInstruments(self, forceUpdate: bool = True) -> str:
622        """
623        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
624        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
625
626        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
627
628        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
629                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
630        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
631        """
632        if self.iListDumpFile is None or not self.iListDumpFile:
633            uLogger.error("Output name of dump file must be defined!")
634            raise Exception("Filename required")
635
636        if not self.iList or forceUpdate:
637            self.iList = self.Listing()
638
639        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
640        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
641            fH.write(jsonDump)
642
643        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
644
645        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
647    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
648        """
649        Show information about one instrument defined by json data and prints it in Markdown format.
650
651        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
652
653        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
654        :param show: if `True` then also printing information about instrument and its current price.
655        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
656        :return: multilines text in Markdown format with information about one instrument.
657        """
658        splitLine = "|                                                             |                                                        |\n"
659        infoText = ""
660
661        if iJSON is not None and iJSON and isinstance(iJSON, dict):
662            info = [
663                "# Main information\n\n",
664                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
665                "| Parameters                                                  | Values                                                 |\n",
666                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
667                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
668                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
669            ]
670
671            if "sector" in iJSON.keys() and iJSON["sector"]:
672                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
673
674            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
675                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
676
677            info.extend([
678                splitLine,
679                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
680                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
681            ])
682
683            if "isin" in iJSON.keys() and iJSON["isin"]:
684                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
685
686            if "classCode" in iJSON.keys():
687                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
688
689            info.extend([
690                splitLine,
691                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
692                splitLine,
693                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
694                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
695                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
696            ])
697
698            if iJSON["figi"]:
699                self._figi = iJSON["figi"]
700                iJSON = iJSON | self.RequestTradingStatus()
701
702                info.extend([
703                    splitLine,
704                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
705                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
706                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
707                ])
708
709            info.append(splitLine)
710
711            if "type" in iJSON.keys() and iJSON["type"]:
712                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
713
714                if "shareType" in iJSON.keys() and iJSON["shareType"]:
715                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
716
717            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
718                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
719
720            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
721                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
722
723            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
724                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
725
726            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
727                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
728
729            if "focusType" in iJSON.keys() and iJSON["focusType"]:
730                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
731
732            if "assetType" in iJSON.keys() and iJSON["assetType"]:
733                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
734
735            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
736                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
737
738            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
739                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
740
741            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
742                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
743
744            if "currency" in iJSON.keys():
745                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
746
747            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
748                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
749
750            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
751                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
752
753            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
754                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
755
756            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
757                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
758
759            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
760                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
761
762            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
763                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
764
765            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
766                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
767
768            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
769                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
770
771            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
772                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
773
774            iExt = None
775            if iJSON["type"] == "Bonds":
776                info.extend([
777                    splitLine,
778                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
779                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
780                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
781                        iJSON["nominal"]["currency"],
782                    )),
783                ])
784
785                if "floatingCouponFlag" in iJSON.keys():
786                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
787
788                if "amortizationFlag" in iJSON.keys():
789                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
790
791                info.append(splitLine)
792
793                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
794                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
795
796                if iJSON["figi"]:
797                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
798
799                    info.extend([
800                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
801                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
802                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
803                    ])
804
805                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
806                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
807                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
808                        iJSON["aciValue"]["currency"]
809                    )))
810
811            if "currentPrice" in iJSON.keys():
812                info.append(splitLine)
813
814                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
815                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
816
817                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
818                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
819                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
820                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
821                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
822
823                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
824                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
825
826                info.extend([
827                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
828                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
829                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
830                    )),
831                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
832                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
833                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
834                    )),
835                    "| Changes between last deal price and last close              | {:<54} |\n".format(
836                        "{:.2f}%{}".format(
837                            iJSON["currentPrice"]["changes"],
838                            " ({}{:.2f} {})".format(
839                                "+" if bondChangesDelta > 0 else "",
840                                bondChangesDelta,
841                                aciCurrency
842                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
843                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
844                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
845                                currency
846                            ),
847                        )
848                    ),
849                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
850                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
851                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
852                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
853                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
854                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
855                    )),
856                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
857                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
858                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
859                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
860                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
861                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
862                    )),
863                ])
864
865            if "lot" in iJSON.keys():
866                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
867
868            if "step" in iJSON.keys() and iJSON["step"] != 0:
869                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
870
871            # Add bond payment calendar:
872            if iJSON["type"] == "Bonds":
873                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
874                info.extend(["\n#", strCalendar])
875
876            infoText += "".join(info)
877
878            if show and not onlyFiles:
879                uLogger.info("{}".format(infoText))
880
881            if self.infoFile is not None and (show or onlyFiles):
882                with open(self.infoFile, "w", encoding="UTF-8") as fH:
883                    fH.write(infoText)
884
885                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
886
887                if self.useHTMLReports:
888                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
889                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
890                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
891
892                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
893
894        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
896    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
897        """
898        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
899
900        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
901        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
902        :return: JSON formatted data with information about instrument.
903        """
904        tickerJSON = {}
905        if self.moreDebug:
906            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
907
908        if not self._ticker:
909            uLogger.warning("self._ticker variable is not be empty!")
910
911        else:
912            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
913                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
914                raise Exception("Instrument not allowed")
915
916            if not self.iList:
917                self.iList = self.Listing()
918
919            if self._ticker in self.iList["Shares"].keys():
920                tickerJSON = self.iList["Shares"][self._ticker]
921                if self.moreDebug:
922                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
923
924            elif self._ticker in self.iList["Currencies"].keys():
925                tickerJSON = self.iList["Currencies"][self._ticker]
926                if self.moreDebug:
927                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
928
929            elif self._ticker in self.iList["Bonds"].keys():
930                tickerJSON = self.iList["Bonds"][self._ticker]
931                if self.moreDebug:
932                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
933
934            elif self._ticker in self.iList["Etfs"].keys():
935                tickerJSON = self.iList["Etfs"][self._ticker]
936                if self.moreDebug:
937                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
938
939            elif self._ticker in self.iList["Futures"].keys():
940                tickerJSON = self.iList["Futures"][self._ticker]
941                if self.moreDebug:
942                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
943
944        if tickerJSON:
945            self._figi = tickerJSON["figi"]
946
947            if requestPrice:
948                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
949
950                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
951                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
952
953                else:
954                    tickerJSON["currentPrice"]["changes"] = 0
955
956            if show:
957                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
958
959        else:
960            if show:
961                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
962
963        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 965    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 966        """
 967        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 968
 969        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 970        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 971        :return: JSON formatted data with information about instrument.
 972        """
 973        figiJSON = {}
 974        if self.moreDebug:
 975            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 976
 977        if not self._figi:
 978            uLogger.warning("self._figi variable is not be empty!")
 979
 980        else:
 981            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 982                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 983                raise Exception("Instrument not allowed")
 984
 985            if not self.iList:
 986                self.iList = self.Listing()
 987
 988            for item in self.iList["Shares"].keys():
 989                if self._figi == self.iList["Shares"][item]["figi"]:
 990                    figiJSON = self.iList["Shares"][item]
 991
 992                    if self.moreDebug:
 993                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 994
 995                    break
 996
 997            if not figiJSON:
 998                for item in self.iList["Currencies"].keys():
 999                    if self._figi == self.iList["Currencies"][item]["figi"]:
1000                        figiJSON = self.iList["Currencies"][item]
1001
1002                        if self.moreDebug:
1003                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1004
1005                        break
1006
1007            if not figiJSON:
1008                for item in self.iList["Bonds"].keys():
1009                    if self._figi == self.iList["Bonds"][item]["figi"]:
1010                        figiJSON = self.iList["Bonds"][item]
1011
1012                        if self.moreDebug:
1013                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1014
1015                        break
1016
1017            if not figiJSON:
1018                for item in self.iList["Etfs"].keys():
1019                    if self._figi == self.iList["Etfs"][item]["figi"]:
1020                        figiJSON = self.iList["Etfs"][item]
1021
1022                        if self.moreDebug:
1023                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1024
1025                        break
1026
1027            if not figiJSON:
1028                for item in self.iList["Futures"].keys():
1029                    if self._figi == self.iList["Futures"][item]["figi"]:
1030                        figiJSON = self.iList["Futures"][item]
1031
1032                        if self.moreDebug:
1033                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1034
1035                        break
1036
1037        if figiJSON:
1038            self._figi = figiJSON["figi"]
1039            self._ticker = figiJSON["ticker"]
1040
1041            if requestPrice:
1042                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1043
1044                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1045                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1046
1047                else:
1048                    figiJSON["currentPrice"]["changes"] = 0
1049
1050            if show:
1051                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1052
1053        else:
1054            if show:
1055                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1056
1057        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1059    def GetCurrentPrices(self, show: bool = True) -> dict:
1060        """
1061        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1062        `{"buy": [{"price": 1243.8, "quantity": 193},
1063                  {"price": 1244.0, "quantity": 168},
1064                  {"price": 1244.8, "quantity": 5},
1065                  {"price": 1245.0, "quantity": 61},
1066                  {"price": 1245.4, "quantity": 60}],
1067          "sell": [{"price": 1243.6, "quantity": 8},
1068                   {"price": 1242.6, "quantity": 10},
1069                   {"price": 1242.4, "quantity": 18},
1070                   {"price": 1242.2, "quantity": 50},
1071                   {"price": 1242.0, "quantity": 113}],
1072          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1073        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1074        - sell: list of dicts with Buyers prices,
1075            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1076            - quantity: volume value by current price in lots,
1077        - limitUp: current trade session limit price, maximum,
1078        - limitDown: current trade session limit price, minimum,
1079        - lastPrice: last deal price of the instrument,
1080        - closePrice: previous trade session close price of the instrument.
1081
1082        See also: `SearchByTicker()` and `SearchByFIGI()`.
1083        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1084        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1085
1086        :param show: if `True` then print DOM to log and console.
1087        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1088                 If an error occurred then returns an empty record:
1089                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1090        """
1091        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1092
1093        if self.depth < 1:
1094            uLogger.error("Depth of Market (DOM) must be >=1!")
1095            raise Exception("Incorrect value")
1096
1097        if not (self._ticker or self._figi):
1098            uLogger.error("self._ticker or self._figi variables must be defined!")
1099            raise Exception("Ticker or FIGI required")
1100
1101        if self._ticker and not self._figi:
1102            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1103            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1104
1105        if not self._ticker and self._figi:
1106            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1107            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1108
1109        if not self._figi:
1110            uLogger.error("FIGI is not defined!")
1111            raise Exception("Ticker or FIGI required")
1112
1113        else:
1114            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1115
1116            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1117            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1118            self.body = str({"figi": self._figi, "depth": self.depth})
1119            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1120
1121            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1122                # list of dicts with sellers orders:
1123                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1124
1125                # list of dicts with buyers orders:
1126                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1127
1128                # max price of instrument at this time:
1129                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1130
1131                # min price of instrument at this time:
1132                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1133
1134                # last price of deal with instrument:
1135                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1136
1137                # last close price of instrument:
1138                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1139
1140            else:
1141                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1142                uLogger.debug("Server response: {}".format(pricesResponse))
1143
1144            if show:
1145                if prices["buy"] or prices["sell"]:
1146                    info = [
1147                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1148                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1149                            self._ticker,
1150                            self._figi,
1151                            self.depth,
1152                        ),
1153                        "-" * 60, "\n",
1154                        "             Orders of Buyers | Orders of Sellers\n",
1155                        "-" * 60, "\n",
1156                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1157                        "-" * 60, "\n",
1158                    ]
1159
1160                    if not prices["buy"]:
1161                        info.append("                              | No orders!\n")
1162                        sumBuy = 0
1163
1164                    else:
1165                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1166                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1167                        for item in maxMinSorted:
1168                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1169
1170                    if not prices["sell"]:
1171                        info.append("No orders!                    |\n")
1172                        sumSell = 0
1173
1174                    else:
1175                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1176                        for item in prices["sell"]:
1177                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1178
1179                    info.extend([
1180                        "-" * 60, "\n",
1181                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1182                        "-" * 60, "\n",
1183                    ])
1184
1185                    infoText = "".join(info)
1186
1187                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1188
1189                else:
1190                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1191
1192        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1194    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1195        """
1196        This method get and show information about all available broker instruments for current user account.
1197        If `instrumentsFile` string is not empty then also save information to this file.
1198
1199        :param show: if `True` then print results to console, if `False` — print only to file.
1200        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1201        :return: multi-lines string with all available broker instruments.
1202        """
1203        if not self.iList:
1204            self.iList = self.Listing()
1205
1206        info = [
1207            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1208            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1209        ]
1210
1211        # add instruments count by type:
1212        for iType in self.iList.keys():
1213            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1214
1215        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1216        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1217
1218        # generating info tables with all instruments by type:
1219        for iType in self.iList.keys():
1220            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1221
1222            for instrument in self.iList[iType].keys():
1223                iName = self.iList[iType][instrument]["name"]  # instrument's name
1224                if len(iName) > 57:
1225                    iName = "{}...".format(iName[:54])  # right trim for a long string
1226
1227                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1228                    self.iList[iType][instrument]["ticker"],
1229                    iName,
1230                    self.iList[iType][instrument]["figi"],
1231                    self.iList[iType][instrument]["currency"],
1232                    self.iList[iType][instrument]["lot"],
1233                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1234                ))
1235
1236        infoText = "".join(info)
1237
1238        if show and not onlyFiles:
1239            uLogger.info(infoText)
1240
1241        if self.instrumentsFile and (show or onlyFiles):
1242            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1243                fH.write(infoText)
1244
1245            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1246
1247            if self.useHTMLReports:
1248                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1249                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1250                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1251
1252                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1253
1254        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multi-lines string with all available broker instruments.

def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1256    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1257        """
1258        This method search and show information about instruments by part of its ticker, FIGI or name.
1259        If `searchResultsFile` string is not empty then also save information to this file.
1260
1261        :param pattern: string with part of ticker, FIGI or instrument's name.
1262        :param show: if `True` then print results to console, if `False` — return list of result only.
1263        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1264        :return: list of dictionaries with all found instruments.
1265        """
1266        if not self.iList:
1267            self.iList = self.Listing()
1268
1269        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1270        compiledPattern = re.compile(pattern, re.IGNORECASE)
1271
1272        for iType in self.iList:
1273            for instrument in self.iList[iType].values():
1274                searchResult = compiledPattern.search(" ".join(
1275                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1276                ))
1277
1278                if searchResult:
1279                    searchResults[iType][instrument["ticker"]] = instrument
1280
1281        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1282        info = [
1283            "# Search results\n\n",
1284            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1285            "* **Search pattern:** [{}]\n".format(pattern),
1286            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1287            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1288        ]
1289        infoShort = info[:]
1290
1291        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1292        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1293        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1294
1295        if resultsLen == 0:
1296            info.append("\nNo results\n")
1297            infoShort.append("\nNo results\n")
1298            uLogger.warning("No results. Try changing your search pattern.")
1299
1300        else:
1301            for iType in searchResults:
1302                iTypeValuesCount = len(searchResults[iType].values())
1303                if iTypeValuesCount > 0:
1304                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1305                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1306
1307                    for instrument in searchResults[iType].values():
1308                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1309                            instrument["type"],
1310                            instrument["ticker"],
1311                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1312                            instrument["figi"],
1313                        ))
1314
1315                    if iTypeValuesCount <= 5:
1316                        infoShort.extend(info[-iTypeValuesCount:])
1317
1318                    else:
1319                        infoShort.extend(info[-5:])
1320                        infoShort.append(skippedLine)
1321
1322        infoText = "".join(info)
1323        infoTextShort = "".join(infoShort)
1324
1325        if show and not onlyFiles:
1326            uLogger.info(infoTextShort)
1327            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1328
1329        if self.searchResultsFile and (show or onlyFiles):
1330            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1331                fH.write(infoText)
1332
1333            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1334
1335            if self.useHTMLReports:
1336                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1337                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1338                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1339
1340                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1341
1342        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1344    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1345        """
1346        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1347
1348        :param instruments: list of strings with tickers or FIGIs.
1349        :return: list with unique instrument FIGIs only.
1350        """
1351        requestedInstruments = []
1352        for iName in instruments:
1353            if iName not in self.aliases.keys():
1354                if iName not in requestedInstruments:
1355                    requestedInstruments.append(iName)
1356
1357            else:
1358                if iName not in requestedInstruments:
1359                    if self.aliases[iName] not in requestedInstruments:
1360                        requestedInstruments.append(self.aliases[iName])
1361
1362        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1363
1364        onlyUniqueFIGIs = []
1365        for iName in requestedInstruments:
1366            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1367                continue
1368
1369            self._ticker = iName
1370            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1371
1372            if not iData:
1373                self._ticker = ""
1374                self._figi = iName
1375
1376                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1377
1378                if not iData:
1379                    self._figi = ""
1380                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1381
1382            if iData and iData["figi"] not in onlyUniqueFIGIs:
1383                onlyUniqueFIGIs.append(iData["figi"])
1384
1385        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1386
1387        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices( self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1389    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1390        """
1391        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1392
1393        See limits: https://tinkoff.github.io/investAPI/limits/
1394
1395        If `pricesFile` string is not empty then also save information to this file.
1396
1397        :param instruments: list of strings with tickers or FIGIs.
1398        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1399        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1400        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1401                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1402        """
1403        if instruments is None or not instruments:
1404            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1405            raise Exception("Ticker or FIGI required")
1406
1407        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1408
1409        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1410
1411        iList = []  # trying to get info and current prices about all unique instruments:
1412        for self._figi in onlyUniqueFIGIs:
1413            iData = self.SearchByFIGI(requestPrice=True, show=False)
1414            iList.append(iData)
1415
1416        self.ShowListOfPrices(iList, show, onlyFiles)
1417
1418        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1420    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1421        """
1422        Show table contains current prices of given instruments.
1423
1424        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1425                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1426        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1427        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1428        :return: multilines text in Markdown format as a table contains current prices.
1429        """
1430        infoText = ""
1431
1432        if show or self.pricesFile or onlyFiles:
1433            info = [
1434                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1435                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1436                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1437            ]
1438
1439            for item in iList:
1440                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1441                    item["ticker"],
1442                    item["figi"],
1443                    item["type"],
1444                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1445                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1446                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1447                    "{} / {}".format(
1448                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1449                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1450                    ),
1451                    "{} / {}".format(
1452                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1453                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1454                    ),
1455                    item["currency"],
1456                ))
1457
1458            infoText = "".join(info)
1459
1460            if show and not onlyFiles:
1461                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1462
1463            if self.pricesFile and (show or onlyFiles):
1464                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1465                    fH.write(infoText)
1466
1467                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1468
1469                if self.useHTMLReports:
1470                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1471                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1472                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1473
1474                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1475
1476        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1478    def RequestTradingStatus(self) -> dict:
1479        """
1480        Requesting trading status for the instrument defined by `figi` variable.
1481
1482        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1483
1484        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1485
1486        :return: dictionary with trading status attributes. Response example:
1487                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1488                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1489        """
1490        if self._figi is None or not self._figi:
1491            uLogger.error("Variable `figi` must be defined for using this method!")
1492            raise Exception("FIGI required")
1493
1494        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1495
1496        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1497        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1498        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1499
1500        if self.moreDebug:
1501            uLogger.debug("Records about current trading status successfully received")
1502
1503        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1505    def RequestPortfolio(self) -> dict:
1506        """
1507        Requesting actual user's portfolio for current `accountId`.
1508
1509        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1510
1511        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1512
1513        :return: dictionary with user's portfolio.
1514        """
1515        if self.accountId is None or not self.accountId:
1516            uLogger.error("Variable `accountId` must be defined for using this method!")
1517            raise Exception("Account ID required")
1518
1519        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1520
1521        self.body = str({"accountId": self.accountId})
1522        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1523        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1524
1525        if self.moreDebug:
1526            uLogger.debug("Records about user's portfolio successfully received")
1527
1528        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1530    def RequestPositions(self) -> dict:
1531        """
1532        Requesting open positions by currencies and instruments for current `accountId`.
1533
1534        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1535
1536        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1537
1538        :return: dictionary with open positions by instruments.
1539        """
1540        if self.accountId is None or not self.accountId:
1541            uLogger.error("Variable `accountId` must be defined for using this method!")
1542            raise Exception("Account ID required")
1543
1544        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1545
1546        self.body = str({"accountId": self.accountId})
1547        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1548        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1549
1550        if self.moreDebug:
1551            uLogger.debug("Records about current open positions successfully received")
1552
1553        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1555    def RequestPendingOrders(self) -> list:
1556        """
1557        Requesting current actual pending limit orders for current `accountId`.
1558
1559        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1560
1561        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1562
1563        :return: list of dictionaries with pending limit orders.
1564        """
1565        if self.accountId is None or not self.accountId:
1566            uLogger.error("Variable `accountId` must be defined for using this method!")
1567            raise Exception("Account ID required")
1568
1569        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1570
1571        self.body = str({"accountId": self.accountId})
1572        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1573        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1574
1575        if "orders" in rawResponse.keys():
1576            rawOrders = rawResponse["orders"]
1577            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1578
1579        else:
1580            rawOrders = []
1581            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1582
1583        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1585    def RequestStopOrders(self) -> list:
1586        """
1587        Requesting current actual stop orders for current `accountId`.
1588
1589        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1590
1591        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1592
1593        :return: list of dictionaries with stop orders.
1594        """
1595        if self.accountId is None or not self.accountId:
1596            uLogger.error("Variable `accountId` must be defined for using this method!")
1597            raise Exception("Account ID required")
1598
1599        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1600
1601        self.body = str({"accountId": self.accountId})
1602        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1603        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1604
1605        if "stopOrders" in rawResponse.keys():
1606            rawStopOrders = rawResponse["stopOrders"]
1607            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1608
1609        else:
1610            rawStopOrders = []
1611            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1612
1613        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full', onlyFiles=False) -> dict:
1615    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1616        """
1617        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1618        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1619        and `overviewBondsCalendarFile` are defined then also save information to file.
1620
1621        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1622        many requests about the state of the portfolio, and then, based on the received data, a large number
1623        of calculation and statistics are collected.
1624
1625        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1626        :param details: how detailed should the information be?
1627        - `full` — shows full available information about portfolio status (by default),
1628        - `positions` — shows only open positions,
1629        - `orders` — shows only sections of open limits and stop orders.
1630        - `digest` — show a short digest of the portfolio status,
1631        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1632        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1633        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1634        :return: dictionary with client's raw portfolio and some statistics.
1635        """
1636        if self.accountId is None or not self.accountId:
1637            uLogger.error("Variable `accountId` must be defined for using this method!")
1638            raise Exception("Account ID required")
1639
1640        view = {
1641            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1642                "headers": {},  # list of dictionaries, response headers without "positions" section
1643                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1644                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1645                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1646                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1647                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1648                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1649                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1650                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1651                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1652            },
1653            "stat": {  # --- some statistics calculated using "raw" sections:
1654                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1655                "availableRUB": 0.,  # available rubles (without other currencies)
1656                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1657                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1658                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1659                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1660                "sharesCostRUB": 0.,  # costs of all shares in RUB
1661                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1662                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1663                "futuresCostRUB": 0.,  # costs of all futures in RUB
1664                "Currencies": [],  # list of dictionaries of all currencies statistics
1665                "Shares": [],  # list of dictionaries of all shares statistics
1666                "Bonds": [],  # list of dictionaries of all bonds statistics
1667                "Etfs": [],  # list of dictionaries of all etfs statistics
1668                "Futures": [],  # list of dictionaries of all futures statistics
1669                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1670                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1671                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1672                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1673                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1674            },
1675            "analytics": {  # --- some analytics of portfolio:
1676                "distrByAssets": {},  # portfolio distribution by assets
1677                "distrByCompanies": {},  # portfolio distribution by companies
1678                "distrBySectors": {},  # portfolio distribution by sectors
1679                "distrByCurrencies": {},  # portfolio distribution by currencies
1680                "distrByCountries": {},  # portfolio distribution by countries
1681                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1682            }
1683        }
1684
1685        details = details.lower()
1686        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1687        if details not in availableDetails:
1688            details = "full"
1689            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1690
1691        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1692
1693        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1694        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1695        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1696        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1697
1698        # save response headers without "positions" section:
1699        for key in portfolioResponse.keys():
1700            if key != "positions":
1701                view["raw"]["headers"][key] = portfolioResponse[key]
1702
1703            else:
1704                continue
1705
1706        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1707        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1708        for item in portfolioResponse["positions"]:
1709            if item["instrumentType"] == "currency":
1710                self._figi = item["figi"]
1711                if not self._figi and item["ticker"]:
1712                    self._ticker = item["ticker"]
1713                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1714
1715                curr = self.SearchByFIGI(requestPrice=False)
1716
1717                # current price of currency in RUB:
1718                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1719                    "name": curr["name"],
1720                    "currentPrice": NanoToFloat(
1721                        item["currentPrice"]["units"],
1722                        item["currentPrice"]["nano"]
1723                    ),
1724                }
1725
1726                view["raw"]["Currencies"].append(item)
1727
1728            elif item["instrumentType"] == "share":
1729                view["raw"]["Shares"].append(item)
1730
1731            elif item["instrumentType"] == "bond":
1732                view["raw"]["Bonds"].append(item)
1733
1734            elif item["instrumentType"] == "etf":
1735                view["raw"]["Etfs"].append(item)
1736
1737            elif item["instrumentType"] == "futures":
1738                view["raw"]["Futures"].append(item)
1739
1740            else:
1741                continue
1742
1743        # how many volume of currencies (by ISO currency name) are blocked:
1744        for item in view["raw"]["positions"]["blocked"]:
1745            blocked = NanoToFloat(item["units"], item["nano"])
1746            if blocked > 0:
1747                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1748
1749        # how many volume of instruments (by FIGI) are blocked:
1750        for item in view["raw"]["positions"]["securities"]:
1751            blocked = int(item["blocked"])
1752            if blocked > 0:
1753                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1754
1755        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1756
1757        if "rub" in allBlocked.keys():
1758            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1759
1760        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1761        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1762        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1763        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1764        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1765        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1766        view["stat"]["portfolioCostRUB"] = sum([
1767            view["stat"]["allCurrenciesCostRUB"],
1768            view["stat"]["sharesCostRUB"],
1769            view["stat"]["bondsCostRUB"],
1770            view["stat"]["etfsCostRUB"],
1771            view["stat"]["futuresCostRUB"],
1772        ])
1773
1774        # --- calculating some portfolio statistics:
1775        byComp = {}  # distribution by companies
1776        bySect = {}  # distribution by sectors
1777        byCurr = {}  # distribution by currencies (include RUB)
1778        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1779        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1780
1781        for item in portfolioResponse["positions"]:
1782            self._figi = item["figi"]
1783            if not self._figi and item["ticker"]:
1784                self._ticker = item["ticker"]
1785                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1786
1787            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1788
1789            if instrument:
1790                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1791                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1792
1793                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1794                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1795
1796                else:
1797                    blocked = 0
1798
1799                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1800                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1801                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1802                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1803                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1804                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1805                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1806                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1807                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1808                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1809                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1810                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1811
1812                statData = {
1813                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1814                    "ticker": instrument["ticker"],  # ticker by FIGI
1815                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1816                    "volume": volume,  # available volume of instrument
1817                    "lots": lots,  # volume in lots of instrument
1818                    "direction": direction,  # direction of an instrument's position: short or long
1819                    "blocked": blocked,  # blocked volume of currency or instrument
1820                    "currentPrice": curPrice,  # current instrument's price in basic asset
1821                    "average": average,  # current average position price
1822                    "cost": cost,  # current cost of all volume of instrument in basic asset
1823                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1824                    "costRUB": costRUB,  # cost of instrument in ruble
1825                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1826                    "profit": profit,  # expected profit at current moment
1827                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1828                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1829                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1830                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1831                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1832                    "step": instrument["step"],  # minimum price increment
1833                }
1834
1835                # adding distribution by unique countries:
1836                if statData["country"] not in byCountry.keys():
1837                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1838
1839                else:
1840                    byCountry[statData["country"]]["cost"] += costRUB
1841                    byCountry[statData["country"]]["percent"] += percentCostRUB
1842
1843                if item["instrumentType"] != "currency":
1844                    # adding distribution by unique companies:
1845                    if statData["name"]:
1846                        if statData["name"] not in byComp.keys():
1847                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1848
1849                        else:
1850                            byComp[statData["name"]]["cost"] += costRUB
1851                            byComp[statData["name"]]["percent"] += percentCostRUB
1852
1853                    # adding distribution by unique sectors:
1854                    if statData["sector"] not in bySect.keys():
1855                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1856
1857                    else:
1858                        bySect[statData["sector"]]["cost"] += costRUB
1859                        bySect[statData["sector"]]["percent"] += percentCostRUB
1860
1861                # adding distribution by unique currencies:
1862                if currency not in byCurr.keys():
1863                    byCurr[currency] = {
1864                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1865                        "cost": costRUB,
1866                        "percent": percentCostRUB
1867                    }
1868
1869                else:
1870                    byCurr[currency]["cost"] += costRUB
1871                    byCurr[currency]["percent"] += percentCostRUB
1872
1873                # saving statistics for every instrument:
1874                if item["instrumentType"] == "currency":
1875                    view["stat"]["Currencies"].append(statData)
1876
1877                    # update dict with free funds for trading (total - blocked) by currencies
1878                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1879                    view["stat"]["funds"][currency] = {
1880                        "total": volume,
1881                        "totalCostRUB": costRUB,  # total volume cost in rubles
1882                        "free": volume - blocked,
1883                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1884                    }
1885
1886                elif item["instrumentType"] == "share":
1887                    view["stat"]["Shares"].append(statData)
1888
1889                elif item["instrumentType"] == "bond":
1890                    view["stat"]["Bonds"].append(statData)
1891
1892                elif item["instrumentType"] == "etf":
1893                    view["stat"]["Etfs"].append(statData)
1894
1895                elif item["instrumentType"] == "Futures":
1896                    view["stat"]["Futures"].append(statData)
1897
1898                else:
1899                    continue
1900
1901        # total changes in Russian Ruble:
1902        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1903        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1904        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1905        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1906        view["stat"]["funds"]["rub"] = {
1907            "total": view["stat"]["availableRUB"],
1908            "totalCostRUB": view["stat"]["availableRUB"],
1909            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1910            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1911        }
1912
1913        # --- pending limit orders sector data:
1914        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1915        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1916
1917        for item in view["raw"]["orders"]:
1918            self._figi = item["figi"]
1919
1920            if item["figi"] not in uniquePendingOrdersFIGIs:
1921                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1922
1923                uniquePendingOrdersFIGIs.append(item["figi"])
1924                uniquePendingOrders[item["figi"]] = instrument
1925
1926            else:
1927                instrument = uniquePendingOrders[item["figi"]]
1928
1929            if instrument:
1930                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1931                orderType = TKS_ORDER_TYPES[item["orderType"]]
1932                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1933                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1934
1935                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1936                if item["direction"] == "ORDER_DIRECTION_BUY":
1937                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1938
1939                else:
1940                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1941
1942                # requested price for order execution:
1943                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1944
1945                # necessary changes in percent to reach target from current price:
1946                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1947
1948                view["stat"]["orders"].append({
1949                    "orderID": item["orderId"],  # orderId number parameter of current order
1950                    "figi": item["figi"],  # FIGI identification
1951                    "ticker": instrument["ticker"],  # ticker name by FIGI
1952                    "lotsRequested": item["lotsRequested"],  # requested lots value
1953                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1954                    "currentPrice": lastPrice,  # current instrument's price for defined action
1955                    "targetPrice": target,  # requested price for order execution in base currency
1956                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1957                    "percentChanges": changes,  # changes in percent to target from current price
1958                    "currency": item["currency"],  # instrument's currency name
1959                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1960                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1961                    "status": orderState,  # order status from TKS_ORDER_STATES
1962                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1963                })
1964
1965        # --- stop orders sector data:
1966        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1967        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1968
1969        for item in view["raw"]["stopOrders"]:
1970            self._figi = item["figi"]
1971
1972            if item["figi"] not in uniqueStopOrdersFIGIs:
1973                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1974
1975                uniqueStopOrdersFIGIs.append(item["figi"])
1976                uniqueStopOrders[item["figi"]] = instrument
1977
1978            else:
1979                instrument = uniqueStopOrders[item["figi"]]
1980
1981            if instrument:
1982                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1983                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1984                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1985
1986                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1987                if "expirationTime" in item.keys():
1988                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1989                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1990
1991                else:
1992                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1993                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1994
1995                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1996                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1997                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1998
1999                else:
2000                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2001
2002                # requested price when stop-order executed:
2003                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2004
2005                # price for limit-order, set up when stop-order executed:
2006                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2007
2008                # necessary changes in percent to reach target from current price:
2009                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2010
2011                view["stat"]["stopOrders"].append({
2012                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2013                    "figi": item["figi"],  # FIGI identification
2014                    "ticker": instrument["ticker"],  # ticker name by FIGI
2015                    "lotsRequested": item["lotsRequested"],  # requested lots value
2016                    "currentPrice": lastPrice,  # current instrument's price for defined action
2017                    "targetPrice": target,  # requested price for stop-order execution in base currency
2018                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2019                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2020                    "percentChanges": changes,  # changes in percent to target from current price
2021                    "currency": item["currency"],  # instrument's currency name
2022                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2023                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2024                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2025                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2026                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2027                })
2028
2029        # --- calculating data for analytics section:
2030        # portfolio distribution by assets:
2031        view["analytics"]["distrByAssets"] = {
2032            "Ruble": {
2033                "uniques": 1,
2034                "cost": view["stat"]["availableRUB"],
2035                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2036            },
2037            "Currencies": {
2038                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2039                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2040                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2041            },
2042            "Shares": {
2043                "uniques": len(view["stat"]["Shares"]),
2044                "cost": view["stat"]["sharesCostRUB"],
2045                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2046            },
2047            "Bonds": {
2048                "uniques": len(view["stat"]["Bonds"]),
2049                "cost": view["stat"]["bondsCostRUB"],
2050                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2051            },
2052            "Etfs": {
2053                "uniques": len(view["stat"]["Etfs"]),
2054                "cost": view["stat"]["etfsCostRUB"],
2055                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2056            },
2057            "Futures": {
2058                "uniques": len(view["stat"]["Futures"]),
2059                "cost": view["stat"]["futuresCostRUB"],
2060                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2061            },
2062        }
2063
2064        # portfolio distribution by companies:
2065        view["analytics"]["distrByCompanies"]["All money cash"] = {
2066            "ticker": "",
2067            "cost": view["stat"]["allCurrenciesCostRUB"],
2068            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2069        }
2070        view["analytics"]["distrByCompanies"].update(byComp)
2071
2072        # portfolio distribution by sectors:
2073        view["analytics"]["distrBySectors"]["All money cash"] = {
2074            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2075            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2076        }
2077        view["analytics"]["distrBySectors"].update(bySect)
2078
2079        # portfolio distribution by currencies:
2080        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2081            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2082
2083            if self.moreDebug:
2084                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2085
2086        view["analytics"]["distrByCurrencies"].update(byCurr)
2087        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2088        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2089
2090        # portfolio distribution by countries:
2091        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2092            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2093
2094            if self.moreDebug:
2095                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2096
2097        view["analytics"]["distrByCountries"].update(byCountry)
2098        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2099        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2100
2101        # --- Prepare text statistics overview in human-readable:
2102        if show or onlyFiles:
2103            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2104
2105            # Whatever the value `details`, header not changes:
2106            info = [
2107                "# Client's portfolio\n\n",
2108                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2109                "* **Account ID:** [{}]\n".format(self.accountId),
2110            ]
2111
2112            if details in ["full", "positions", "digest"]:
2113                info.extend([
2114                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2115                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2116                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2117                        view["stat"]["totalChangesRUB"],
2118                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2119                        view["stat"]["totalChangesPercentRUB"],
2120                    ),
2121                ])
2122
2123            if details in ["full", "positions"]:
2124                info.extend([
2125                    "## Open positions\n\n",
2126                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2127                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2128                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2129                        "{:.2f} ({:.2f}) rub".format(
2130                            view["stat"]["availableRUB"],
2131                            view["stat"]["blockedRUB"],
2132                        )
2133                    )
2134                ])
2135
2136                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2137                    return [
2138                        "|                             |                                 |          |              |              |                     |                              |\n",
2139                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2140                            noTradeStr if noTradeStr else typeStr,
2141                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2142                        ),
2143                    ]
2144
2145                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2146                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2147                        "{} [{}]".format(data["ticker"], data["figi"]),
2148                        "{:.2f} ({:.2f}) {}".format(
2149                            data["volume"],
2150                            data["blocked"],
2151                            data["currency"],
2152                        ) if isCurr else "{:.0f} ({:.0f})".format(
2153                            data["volume"],
2154                            data["blocked"],
2155                        ),
2156                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2157                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2158                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2159                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2160                        "{}{:.2f} {} ({}{:.2f}%)".format(
2161                            "+" if data["profit"] > 0 else "",
2162                            data["profit"], data["baseCurrencyName"],
2163                            "+" if data["percentProfit"] > 0 else "",
2164                            data["percentProfit"],
2165                        ),
2166                    )
2167
2168                # --- Show currencies section:
2169                if view["stat"]["Currencies"]:
2170                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2171                    for item in view["stat"]["Currencies"]:
2172                        info.append(_InfoStr(item, isCurr=True))
2173
2174                else:
2175                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2176
2177                # --- Show shares section:
2178                if view["stat"]["Shares"]:
2179                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2180
2181                    for item in view["stat"]["Shares"]:
2182                        info.append(_InfoStr(item))
2183
2184                else:
2185                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2186
2187                # --- Show bonds section:
2188                if view["stat"]["Bonds"]:
2189                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2190
2191                    for item in view["stat"]["Bonds"]:
2192                        info.append(_InfoStr(item))
2193
2194                else:
2195                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2196
2197                # --- Show etfs section:
2198                if view["stat"]["Etfs"]:
2199                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2200
2201                    for item in view["stat"]["Etfs"]:
2202                        info.append(_InfoStr(item))
2203
2204                else:
2205                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2206
2207                # --- Show futures section:
2208                if view["stat"]["Futures"]:
2209                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2210
2211                    for item in view["stat"]["Futures"]:
2212                        info.append(_InfoStr(item))
2213
2214                else:
2215                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2216
2217            if details in ["full", "orders"]:
2218                # --- Show pending limit orders section:
2219                if view["stat"]["orders"]:
2220                    info.extend([
2221                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2222                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2223                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2224                    ])
2225
2226                    for item in view["stat"]["orders"]:
2227                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2228                            "{} [{}]".format(item["ticker"], item["figi"]),
2229                            item["orderID"],
2230                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2231                            "{} {} ({}{:.2f}%)".format(
2232                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2233                                item["baseCurrencyName"],
2234                                "+" if item["percentChanges"] > 0 else "",
2235                                float(item["percentChanges"]),
2236                            ),
2237                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2238                            item["action"],
2239                            item["type"],
2240                            item["date"],
2241                        ))
2242
2243                else:
2244                    info.append("\n## Total pending limit-orders: [0]\n")
2245
2246                # --- Show stop orders section:
2247                if view["stat"]["stopOrders"]:
2248                    info.extend([
2249                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2250                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2251                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2252                    ])
2253
2254                    for item in view["stat"]["stopOrders"]:
2255                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2256                            "{} [{}]".format(item["ticker"], item["figi"]),
2257                            item["orderID"],
2258                            item["lotsRequested"],
2259                            "{} {} ({}{:.2f}%)".format(
2260                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2261                                item["baseCurrencyName"],
2262                                "+" if item["percentChanges"] > 0 else "",
2263                                float(item["percentChanges"]),
2264                            ),
2265                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2266                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2267                            item["action"],
2268                            item["type"],
2269                            item["expType"],
2270                            item["createDate"],
2271                            item["expDate"],
2272                        ))
2273
2274                else:
2275                    info.append("\n## Total stop-orders: [0]\n")
2276
2277            if details in ["full", "analytics"]:
2278                # -- Show analytics section:
2279                if view["stat"]["portfolioCostRUB"] > 0:
2280                    info.extend([
2281                        "\n# Analytics\n\n"
2282                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2283                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2284                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2285                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2286                            view["stat"]["totalChangesRUB"],
2287                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2288                            view["stat"]["totalChangesPercentRUB"],
2289                        ),
2290                        "\n## Portfolio distribution by assets\n"
2291                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2292                        "|------------------------------------|---------|---------|--------------------|\n",
2293                    ])
2294
2295                    for key in view["analytics"]["distrByAssets"].keys():
2296                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2297                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2298                                key,
2299                                view["analytics"]["distrByAssets"][key]["uniques"],
2300                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2301                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2302                            ))
2303
2304                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2305
2306                    info.extend([
2307                        "\n## Portfolio distribution by companies\n"
2308                        "\n| Company                                      | Percent | Current cost       |\n",
2309                        aSepLine,
2310                    ])
2311
2312                    for company in view["analytics"]["distrByCompanies"].keys():
2313                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2314                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2315                                "{}{}".format(
2316                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2317                                    company,
2318                                ),
2319                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2320                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2321                            ))
2322
2323                    info.extend([
2324                        "\n## Portfolio distribution by sectors\n"
2325                        "\n| Sector                                       | Percent | Current cost       |\n",
2326                        aSepLine,
2327                    ])
2328
2329                    for sector in view["analytics"]["distrBySectors"].keys():
2330                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2331                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2332                                sector,
2333                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2334                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2335                            ))
2336
2337                    info.extend([
2338                        "\n## Portfolio distribution by currencies\n"
2339                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2340                        aSepLine,
2341                    ])
2342
2343                    for curr in view["analytics"]["distrByCurrencies"].keys():
2344                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2345                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2346                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2347                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2348                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2349                            ))
2350
2351                    info.extend([
2352                        "\n## Portfolio distribution by countries\n"
2353                        "\n| Assets by country                            | Percent | Current cost       |\n",
2354                        aSepLine,
2355                    ])
2356
2357                    for country in view["analytics"]["distrByCountries"].keys():
2358                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2359                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2360                                country,
2361                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2362                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2363                            ))
2364
2365            if details in ["full", "calendar"]:
2366                # -- Show bonds payment calendar section:
2367                if view["stat"]["Bonds"]:
2368                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2369                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2370                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2371
2372                else:
2373                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2374
2375            infoText = "".join(info)
2376
2377            if show and not onlyFiles:
2378                uLogger.info(infoText)
2379
2380            if details == "full" and self.overviewFile:
2381                filename = self.overviewFile
2382
2383            elif details == "digest" and self.overviewDigestFile:
2384                filename = self.overviewDigestFile
2385
2386            elif details == "positions" and self.overviewPositionsFile:
2387                filename = self.overviewPositionsFile
2388
2389            elif details == "orders" and self.overviewOrdersFile:
2390                filename = self.overviewOrdersFile
2391
2392            elif details == "analytics" and self.overviewAnalyticsFile:
2393                filename = self.overviewAnalyticsFile
2394
2395            elif details == "calendar" and self.overviewBondsCalendarFile:
2396                filename = self.overviewBondsCalendarFile
2397
2398            else:
2399                filename = ""
2400
2401            if filename and (show or onlyFiles):
2402                with open(filename, "w", encoding="UTF-8") as fH:
2403                    fH.write(infoText)
2404
2405                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2406
2407                if self.useHTMLReports:
2408                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2409                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2410                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2411
2412                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2413
2414        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio).
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2416    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2417        """
2418        Returns history operations between two given dates for current `accountId`.
2419        If `reportFile` string is not empty then also save human-readable report.
2420        Shows some statistical data of closed positions.
2421
2422        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2423        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2424        :param show: if `True` then also prints all records to the console.
2425        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2426        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2427        :return: original list of dictionaries with history of deals records from API ("operations" key):
2428                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2429                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2430        """
2431        if self.accountId is None or not self.accountId:
2432            uLogger.error("Variable `accountId` must be defined for using this method!")
2433            raise Exception("Account ID required")
2434
2435        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2436
2437        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2438
2439        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2440        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2441        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2442        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2443        customStat = {}  # custom statistics in additional to responseJSON
2444
2445        # --- output report in human-readable format:
2446        if show or onlyFiles or self.reportFile:
2447            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2448            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2449            nextDay = ""
2450
2451            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2452
2453            if len(ops) > 0:
2454                customStat = {
2455                    "opsCount": 0,  # total operations count
2456                    "buyCount": 0,  # buy operations
2457                    "sellCount": 0,  # sell operations
2458                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2459                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2460                    "payIn": {"rub": 0.},  # Deposit brokerage account
2461                    "payOut": {"rub": 0.},  # Withdrawals
2462                    "divs": {"rub": 0.},  # Dividends income
2463                    "coupons": {"rub": 0.},  # Coupon's income
2464                    "brokerCom": {"rub": 0.},  # Service commissions
2465                    "serviceCom": {"rub": 0.},  # Service commissions
2466                    "marginCom": {"rub": 0.},  # Margin commissions
2467                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2468                }
2469
2470                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2471                for item in ops:
2472                    if item["state"] == "OPERATION_STATE_EXECUTED":
2473                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2474
2475                        # count buy operations:
2476                        if "_BUY" in item["operationType"]:
2477                            customStat["buyCount"] += 1
2478
2479                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2480                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2484
2485                        # count sell operations:
2486                        elif "_SELL" in item["operationType"]:
2487                            customStat["sellCount"] += 1
2488
2489                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2490                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2494
2495                        # count incoming operations:
2496                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2497                            if item["payment"]["currency"] in customStat["payIn"].keys():
2498                                customStat["payIn"][item["payment"]["currency"]] += payment
2499
2500                            else:
2501                                customStat["payIn"][item["payment"]["currency"]] = payment
2502
2503                        # count withdrawals operations:
2504                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2505                            if item["payment"]["currency"] in customStat["payOut"].keys():
2506                                customStat["payOut"][item["payment"]["currency"]] += payment
2507
2508                            else:
2509                                customStat["payOut"][item["payment"]["currency"]] = payment
2510
2511                        # count dividends income:
2512                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2513                            if item["payment"]["currency"] in customStat["divs"].keys():
2514                                customStat["divs"][item["payment"]["currency"]] += payment
2515
2516                            else:
2517                                customStat["divs"][item["payment"]["currency"]] = payment
2518
2519                        # count coupon's income:
2520                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2521                            if item["payment"]["currency"] in customStat["coupons"].keys():
2522                                customStat["coupons"][item["payment"]["currency"]] += payment
2523
2524                            else:
2525                                customStat["coupons"][item["payment"]["currency"]] = payment
2526
2527                        # count broker commissions:
2528                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2529                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2530                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2531
2532                            else:
2533                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2534
2535                        # count service commissions:
2536                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2537                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2538                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2539
2540                            else:
2541                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2542
2543                        # count margin commissions:
2544                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2545                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2546                                customStat["marginCom"][item["payment"]["currency"]] += payment
2547
2548                            else:
2549                                customStat["marginCom"][item["payment"]["currency"]] = payment
2550
2551                        # count withholding taxes:
2552                        elif "_TAX" in item["operationType"]:
2553                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2554                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2555
2556                            else:
2557                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2558
2559                        else:
2560                            continue
2561
2562                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2563
2564                # --- view "Actions" lines:
2565                info.extend([
2566                    "| Report sections            |                               |                              |                      |                        |\n",
2567                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2568                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2569                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2570                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2571                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2572                    ),
2573                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2574                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2575                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2576                    ),
2577                ])
2578
2579                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2580                for key in opsKeys:
2581                    if key == "rub":
2582                        continue
2583
2584                    info.extend([
2585                        "|                            |                               | {:<28} |                      |                        |\n".format(
2586                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2587                        ),
2588                        "|                            |                               | {:<28} |                      |                        |\n".format(
2589                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2590                        ),
2591                    ])
2592
2593                info.append(splitLine1)
2594
2595                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2596                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2597                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2598                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2599                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2600                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2601                    )
2602
2603                # --- view "Payments" lines:
2604                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2605                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2606
2607                for key in paymentsKeys:
2608                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2609
2610                info.append(splitLine1)
2611
2612                # --- view "Commissions and taxes" lines:
2613                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2614                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2615
2616                for key in comKeys:
2617                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2618
2619                info.extend([
2620                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2621                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2622                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2623                ])
2624
2625            else:
2626                info.append("Broker returned no operations during this period\n")
2627
2628            # --- view "Operations" section:
2629            for item in ops:
2630                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2631                    continue
2632
2633                else:
2634                    self._figi = item["figi"]
2635                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2636                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2637
2638                    # group of deals during one day:
2639                    if nextDay and item["date"].split("T")[0] != nextDay:
2640                        info.append(splitLine2)
2641                        nextDay = ""
2642
2643                    else:
2644                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2645
2646                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2647                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2648                        self._figi if self._figi else "—",
2649                        instrument["ticker"] if instrument else "—",
2650                        instrument["type"] if instrument else "—",
2651                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2652                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2653                        TKS_OPERATION_STATES[item["state"]],
2654                        TKS_OPERATION_TYPES[item["operationType"]],
2655                    ))
2656
2657            infoText = "".join(info)
2658
2659            if show and not onlyFiles:
2660                if self.moreDebug:
2661                    uLogger.debug("Records about history of a client's operations successfully received")
2662
2663                uLogger.info(infoText)
2664
2665            if self.reportFile and (show or onlyFiles):
2666                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2667                    fH.write(infoText)
2668
2669                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2670
2671                if self.useHTMLReports:
2672                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2673                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2674                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2675
2676                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2677
2678        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False, onlyFiles=False) -> pandas.core.frame.DataFrame:
2680    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2681        """
2682        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2683
2684        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2685        Warning! Broker server used ISO UTC time by default.
2686
2687        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2688        Also, `historyFile` used to update history with `onlyMissing` parameter.
2689
2690        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2691
2692        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2693        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2694        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2695                         `"hour"`, `"day"`. Default: `"hour"`.
2696        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2697                            False by default. Warning! History appends only from last candle to current time
2698                            with always update last candle!
2699        :param csvSep: separator if csv-file is used, `,` by default.
2700        :param show: if `True` then also prints Pandas DataFrame to the console.
2701        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2702        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2703                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2704        """
2705        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2706        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2707        history = None  # empty pandas object for history
2708
2709        if interval not in TKS_CANDLE_INTERVALS.keys():
2710            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2711            raise Exception("Incorrect value")
2712
2713        if not (self._ticker or self._figi):
2714            uLogger.error("Ticker or FIGI must be defined!")
2715            raise Exception("Ticker or FIGI required")
2716
2717        if self._ticker and not self._figi:
2718            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2719            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2720
2721        if self._figi and not self._ticker:
2722            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2723            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2724
2725        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2726        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2727        if interval.lower() != "day":
2728            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2729
2730        delta = dtEnd - dtStart  # current UTC time minus last time in file
2731        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2732
2733        # calculate history length in candles:
2734        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2735        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2736            length += 1  # to avoid fraction time
2737
2738        # calculate data blocks count:
2739        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2740
2741        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2742        if self.moreDebug:
2743            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2744            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2745            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2746            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2747
2748        tempOld = None  # pandas object for old history, if --only-missing key present
2749        lastTime = None  # datetime object of last old candle in file
2750
2751        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2752            if self.moreDebug:
2753                uLogger.debug("--only-missing key present, add only last missing candles...")
2754                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2755
2756            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2757
2758            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2759            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2760            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2761            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2762
2763            # get last datetime object from last string in file or minus 1 delta if file is empty:
2764            if len(tempOld) > 0:
2765                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2766
2767            else:
2768                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2769
2770            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2771
2772        responseJSONs = []  # raw history blocks of data
2773
2774        blockEnd = dtEnd
2775        for item in range(blocks):
2776            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2777            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2778
2779            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2780                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2781            ))
2782
2783            if blockStart == blockEnd:
2784                uLogger.debug("Skipped this zero-length block...")
2785
2786            else:
2787                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2788                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2789                self.body = str({
2790                    "figi": self._figi,
2791                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2792                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2793                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2794                })
2795                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2796
2797                if "code" in responseJSON.keys():
2798                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2799
2800                else:
2801                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2802                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2803
2804                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2805
2806            blockEnd = blockStart
2807
2808        printCount = len(responseJSONs)  # candles to show in console
2809        if responseJSONs:
2810            tempHistory = pd.DataFrame(
2811                data={
2812                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2813                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2814                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2815                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2816                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2817                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2818                    "volume": [int(item["volume"]) for item in responseJSONs],
2819                },
2820                index=range(len(responseJSONs)),
2821                columns=["date", "time", "open", "high", "low", "close", "volume"],
2822            )
2823            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2824            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2825
2826            # append only newest candles to old history if --only-missing key present:
2827            if onlyMissing and tempOld is not None and lastTime is not None:
2828                index = 0  # find start index in tempHistory data:
2829
2830                for i, item in tempHistory.iterrows():
2831                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2832
2833                    if curTime == lastTime:
2834                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2835                        index = i
2836                        printCount = index + 1
2837                        break
2838
2839                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2840
2841            else:
2842                history = tempHistory  # if no `--only-missing` key then load full data from server
2843
2844            if self.moreDebug:
2845                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2846
2847        if history is not None and not history.empty:
2848            if show and not onlyFiles:
2849                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2850                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2851                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2852                ))
2853
2854        else:
2855            uLogger.warning("Received an empty candles history!")
2856
2857        if self.historyFile is not None:
2858            if history is not None and not history.empty:
2859                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2860                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2861
2862            else:
2863                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2864
2865        else:
2866            if self.moreDebug:
2867                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2868
2869        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2871    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2872        """
2873        Load candles history from csv-file and return Pandas DataFrame object.
2874
2875        See also: `History()` and `ShowHistoryChart()` methods.
2876
2877        :param filePath: path to csv-file to open.
2878        """
2879        loadedHistory = None  # init candles data object
2880
2881        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2882
2883        if os.path.exists(filePath):
2884            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2885
2886            tfStr = self.priceModel.FormattedDelta(
2887                self.priceModel.timeframe,
2888                "{days} days {hours}h {minutes}m {seconds}s",
2889            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2890                self.priceModel.timeframe,
2891                "{hours}h {minutes}m {seconds}s",
2892            )
2893
2894            if loadedHistory is not None and not loadedHistory.empty:
2895                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2896                    len(loadedHistory),
2897                    tfStr,
2898                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2899                )
2900
2901            else:
2902                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2903
2904        else:
2905            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2906
2907        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2909    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2910        """
2911        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2912
2913        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2914        Default: `index.html` (both for interact and non-interact candlesticks chart).
2915
2916        See also: `History()` and `LoadHistory()` methods.
2917
2918        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2919        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2920                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2921                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2922                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2923        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2924                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2925        """
2926        if isinstance(candles, str):
2927            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2928            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2929
2930        elif isinstance(candles, pd.DataFrame):
2931            self.priceModel.prices = candles  # set candles chain from variable
2932            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2933
2934            if "datetime" not in candles.columns:
2935                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2936
2937        else:
2938            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2939            raise Exception("Incorrect value")
2940
2941        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2942
2943        if interact:
2944            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2945
2946            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2947
2948        else:
2949            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2950
2951            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2952
2953        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2955    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2956        """
2957        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2958        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2959
2960        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2961
2962        :param operation: string "Buy" or "Sell".
2963        :param lots: volume, integer count of lots >= 1.
2964        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2965        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2966        :param expDate: string "Undefined" by default or local date in future,
2967                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2968        :return: JSON with response from broker server.
2969        """
2970        if self.accountId is None or not self.accountId:
2971            uLogger.error("Variable `accountId` must be defined for using this method!")
2972            raise Exception("Account ID required")
2973
2974        if operation is None or not operation or operation not in ("Buy", "Sell"):
2975            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2976            raise Exception("Incorrect value")
2977
2978        if lots is None or lots < 1:
2979            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2980            lots = 1
2981
2982        if tp is None or tp < 0:
2983            tp = 0
2984
2985        if sl is None or sl < 0:
2986            sl = 0
2987
2988        if expDate is None or not expDate:
2989            expDate = "Undefined"
2990
2991        if not (self._ticker or self._figi):
2992            uLogger.error("Ticker or FIGI must be defined!")
2993            raise Exception("Ticker or FIGI required")
2994
2995        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2996        self._ticker = instrument["ticker"]
2997        self._figi = instrument["figi"]
2998
2999        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3000
3001        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3002        self.body = str({
3003            "figi": self._figi,
3004            "quantity": str(lots),
3005            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3006            "accountId": str(self.accountId),
3007            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3008        })
3009        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3010
3011        if "orderId" in response.keys():
3012            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3013                operation, response["orderId"],
3014                self._ticker, self._figi, lots,
3015                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3016                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3017                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3018            ))
3019
3020            if tp > 0:
3021                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3022
3023            if sl > 0:
3024                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3025
3026        else:
3027            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3028
3029        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3031    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3032        """
3033        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3034        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3035
3036        See also: `Order()` and `Trade()` docstrings.
3037
3038        :param lots: volume, integer count of lots >= 1.
3039        :param tp: float > 0, take profit price of stop-order.
3040        :param sl: float > 0, stop loss price of stop-order.
3041        :param expDate: it's a local date in future.
3042                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3043        :return: JSON with response from broker server.
3044        """
3045        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3047    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3048        """
3049        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3050        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3051
3052        See also: `Order()` and `Trade()` docstrings.
3053
3054        :param lots: volume, integer count of lots >= 1.
3055        :param tp: float > 0, take profit price of stop-order.
3056        :param sl: float > 0, stop loss price of stop-order.
3057        :param expDate: it's a local date in the future.
3058                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3059        :return: JSON with response from broker server.
3060        """
3061        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3063    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3064        """
3065        Close position of given instruments.
3066
3067        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3068        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3069                         This avoids unnecessary downloading data from the server.
3070        """
3071        if instruments is None or not instruments:
3072            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3073            raise Exception("Ticker or FIGI required")
3074
3075        if isinstance(instruments, str):
3076            instruments = [instruments]
3077
3078        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3079        if uniqueInstruments:
3080            if portfolio is None or not portfolio:
3081                portfolio = self.Overview(show=False)
3082
3083            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3084            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3085
3086            for self._figi in uniqueInstruments:
3087                if self._figi not in allOpened:
3088                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3089                    continue
3090
3091                # search open trade info about instrument by ticker:
3092                instrument = {}
3093                for iType in TKS_INSTRUMENTS:
3094                    if instrument:
3095                        break
3096
3097                    for item in portfolio["stat"][iType]:
3098                        if item["figi"] == self._figi:
3099                            instrument = item
3100                            break
3101
3102                if instrument:
3103                    self._ticker = instrument["ticker"]
3104                    self._figi = instrument["figi"]
3105
3106                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3107                        self._ticker,
3108                        self._figi,
3109                        int(instrument["volume"]),
3110                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3111                    ))
3112
3113                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3114
3115                    if tradeLots > 0:
3116                        if instrument["blocked"] > 0:
3117                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3118                                instrument["blocked"],
3119                                self._ticker,
3120                                tradeLots,
3121                            ))
3122
3123                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3124                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3125
3126                    else:
3127                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3129    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3130        """
3131        Close all positions of given instruments with defined type.
3132
3133        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3134        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3135                         This avoids unnecessary downloading data from the server.
3136        """
3137        if iType not in TKS_INSTRUMENTS:
3138            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3139
3140        else:
3141            if portfolio is None or not portfolio:
3142                portfolio = self.Overview(show=False)
3143
3144            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3145            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3146
3147            if tickers and portfolio:
3148                self.CloseTrades(tickers, portfolio)
3149
3150            else:
3151                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3153    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3154        """
3155        Universal method to create market or limit orders with all available parameters for current `accountId`.
3156        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3157
3158        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3159        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3160
3161        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3162        then broker immediately open market order as you can do simple --buy or --sell operations!
3163
3164        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3165        When current price will go up or down to target price value then broker opens a limit order.
3166        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3167
3168        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3169
3170        :param operation: string "Buy" or "Sell".
3171        :param orderType: string "Limit" or "Stop".
3172        :param lots: volume, integer count of lots >= 1.
3173        :param targetPrice: target price > 0. This is open trade price for limit order.
3174        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3175                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3176        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3177                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3178                         Stop loss order always executed by market price.
3179        :param expDate: string "Undefined" by default or local date in future.
3180                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3181                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3182                        A limit order has no expiration date, it lasts until the end of the trading day.
3183        :return: JSON with response from broker server.
3184        """
3185        if self.accountId is None or not self.accountId:
3186            uLogger.error("Variable `accountId` must be defined for using this method!")
3187            raise Exception("Account ID required")
3188
3189        if operation is None or not operation or operation not in ("Buy", "Sell"):
3190            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3191            raise Exception("Incorrect value")
3192
3193        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3194            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3195            raise Exception("Incorrect value")
3196
3197        if lots is None or lots < 1:
3198            uLogger.error("You must define trade volume > 0: integer count of lots!")
3199            raise Exception("Incorrect value")
3200
3201        if targetPrice is None or targetPrice <= 0:
3202            uLogger.error("Target price for limit-order must be greater than 0!")
3203            raise Exception("Incorrect value")
3204
3205        if limitPrice is None or limitPrice <= 0:
3206            limitPrice = targetPrice
3207
3208        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3209            stopType = "Limit"
3210
3211        if expDate is None or not expDate:
3212            expDate = "Undefined"
3213
3214        if not (self._ticker or self._figi):
3215            uLogger.error("Tocker or FIGI must be defined!")
3216            raise Exception("Ticker or FIGI required")
3217
3218        response = {}
3219        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3220        self._ticker = instrument["ticker"]
3221        self._figi = instrument["figi"]
3222
3223        if orderType == "Limit":
3224            uLogger.debug(
3225                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3226                    self._ticker, self._figi,
3227                    operation, lots, targetPrice, instrument["currency"],
3228                ))
3229
3230            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3231            self.body = str({
3232                "figi": self._figi,
3233                "quantity": str(lots),
3234                "price": FloatToNano(targetPrice),
3235                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3236                "accountId": str(self.accountId),
3237                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3238            })
3239            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3240
3241            if "orderId" in response.keys():
3242                uLogger.info(
3243                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3244                        response["orderId"], self._ticker, self._figi, operation, lots,
3245                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3246                    ))
3247
3248                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3249                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3250                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3251                            targetPrice, instrument["currency"],
3252                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3253                        ))
3254
3255                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3256                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3257                            targetPrice, instrument["currency"],
3258                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3259                        ))
3260
3261            else:
3262                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3263
3264        if orderType == "Stop":
3265            uLogger.debug(
3266                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3267                    self._ticker, self._figi,
3268                    operation, lots,
3269                    targetPrice, instrument["currency"],
3270                    limitPrice, instrument["currency"],
3271                    stopType, expDate,
3272                ))
3273
3274            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3275            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3276            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3277
3278            body = {
3279                "figi": self._figi,
3280                "quantity": str(lots),
3281                "price": FloatToNano(limitPrice),
3282                "stopPrice": FloatToNano(targetPrice),
3283                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3284                "accountId": str(self.accountId),
3285                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3286                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3287            }
3288
3289            if expDateUTC:
3290                body["expireDate"] = expDateUTC
3291
3292            self.body = str(body)
3293            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3294
3295            if "stopOrderId" in response.keys():
3296                uLogger.info(
3297                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3298                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3299                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3300                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3301                        TKS_STOP_ORDER_TYPES[stopOrderType],
3302                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3303                    ))
3304
3305                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3306                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3307                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3308                            targetPrice, instrument["currency"],
3309                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3310                        ))
3311
3312                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3313                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3314                            targetPrice, instrument["currency"],
3315                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3316                        ))
3317
3318            else:
3319                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3320
3321        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3323    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3324        """
3325        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3326        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3327        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3328        See also: `Order()` docstring.
3329
3330        :param lots: volume, integer count of lots >= 1.
3331        :param targetPrice: target price > 0. This is open trade price for limit order.
3332        :return: JSON with response from broker server.
3333        """
3334        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3336    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3337        """
3338        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3339        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3340        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3341        target price value then broker opens a limit order. See also: `Order()` docstring.
3342
3343        :param lots: volume, integer count of lots >= 1.
3344        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3345        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3346                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3347        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3348                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3349        :param expDate: string "Undefined" by default or local date in future.
3350                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3351                        This date is converting to UTC format for server.
3352        :return: JSON with response from broker server.
3353        """
3354        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3356    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3357        """
3358        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3359        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3360        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3361        See also: `Order()` docstring.
3362
3363        :param lots: volume, integer count of lots >= 1.
3364        :param targetPrice: target price > 0. This is open trade price for limit order.
3365        :return: JSON with response from broker server.
3366        """
3367        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3369    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3370        """
3371        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3372        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3373        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3374        target price value then broker opens a limit order. See also: `Order()` docstring.
3375
3376        :param lots: volume, integer count of lots >= 1.
3377        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3378        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3379                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3380        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3381                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3382        :param expDate: string "Undefined" by default or local date in future.
3383                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3384                        This date is converting to UTC format for server.
3385        :return: JSON with response from broker server.
3386        """
3387        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3389    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3390        """
3391        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3392
3393        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3394        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3395                             This avoids unnecessary downloading data from the server.
3396        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3397        """
3398        if self.accountId is None or not self.accountId:
3399            uLogger.error("Variable `accountId` must be defined for using this method!")
3400            raise Exception("Account ID required")
3401
3402        if orderIDs:
3403            if allOrdersIDs is None:
3404                rawOrders = self.RequestPendingOrders()
3405                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3406
3407            if allStopOrdersIDs is None:
3408                rawStopOrders = self.RequestStopOrders()
3409                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3410
3411            for orderID in orderIDs:
3412                idInPendingOrders = orderID in allOrdersIDs
3413                idInStopOrders = orderID in allStopOrdersIDs
3414
3415                if not (idInPendingOrders or idInStopOrders):
3416                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3417                    continue
3418
3419                else:
3420                    if idInPendingOrders:
3421                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3422
3423                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3424                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3425                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3426                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3427
3428                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3429                            if self.moreDebug:
3430                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3431
3432                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3433
3434                        else:
3435                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3436
3437                    elif idInStopOrders:
3438                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3439
3440                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3441                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3442                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3443                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3444
3445                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3446                            if self.moreDebug:
3447                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3448
3449                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3450
3451                        else:
3452                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3453
3454                    else:
3455                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3457    def CloseAllOrders(self) -> None:
3458        """
3459        Gets a list of open pending and stop orders and cancel it all.
3460        """
3461        rawOrders = self.RequestPendingOrders()
3462        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3463        lenOrders = len(allOrdersIDs)
3464
3465        rawStopOrders = self.RequestStopOrders()
3466        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3467        lenSOrders = len(allStopOrdersIDs)
3468
3469        if lenOrders > 0 or lenSOrders > 0:
3470            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3471
3472            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3473
3474        else:
3475            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3477    def CloseAll(self, *args) -> None:
3478        """
3479        Close all available (not blocked) opened trades and orders.
3480
3481        Also, you can select one or more keywords case-insensitive:
3482        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3483
3484        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3485        """
3486        overview = self.Overview(show=False)  # get all open trades info
3487
3488        if len(args) == 0:
3489            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3490            self.CloseAllOrders()  # close all pending and stop orders
3491
3492            for iType in TKS_INSTRUMENTS:
3493                if iType != "Currencies":
3494                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3495
3496        else:
3497            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3498            lowerArgs = [x.lower() for x in args]
3499
3500            if "orders" in lowerArgs:
3501                self.CloseAllOrders()  # close all pending and stop orders
3502
3503            for iType in TKS_INSTRUMENTS:
3504                if iType.lower() in lowerArgs and iType != "Currencies":
3505                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3507    def CloseAllByTicker(self, instrument: str) -> None:
3508        """
3509        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3510
3511        This method searches opened trade and orders of instrument throw all portfolio and then use
3512        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3513
3514        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3515
3516        :param instrument: string with ticker.
3517        """
3518        if instrument is None or not instrument:
3519            uLogger.error("Ticker name must be defined for using this method!")
3520            raise Exception("Ticker required")
3521
3522        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3523
3524        self._ticker = instrument  # try to set instrument as ticker
3525        self._figi = ""
3526
3527        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3528        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3529
3530        if limitAll and self.IsInLimitOrders(portfolio=overview):
3531            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3532            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3533
3534        if stopAll and self.IsInStopOrders(portfolio=overview):
3535            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3536            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3537
3538        if self.IsInPortfolio(portfolio=overview):
3539            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3540            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3542    def CloseAllByFIGI(self, instrument: str) -> None:
3543        """
3544        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3545
3546        This method searches opened trade and orders of instrument throw all portfolio and then use
3547        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3548
3549        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3550
3551        :param instrument: string with FIGI id.
3552        """
3553        if instrument is None or not instrument:
3554            uLogger.error("FIGI id must be defined for using this method!")
3555            raise Exception("FIGI required")
3556
3557        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3558
3559        self._ticker = ""
3560        self._figi = instrument  # try to set instrument as FIGI id
3561
3562        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3563        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3564
3565        if limitAll and self.IsInLimitOrders(portfolio=overview):
3566            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3567            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3568
3569        if stopAll and self.IsInStopOrders(portfolio=overview):
3570            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3571            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3572
3573        if self.IsInPortfolio(portfolio=overview):
3574            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3575            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3577    @staticmethod
3578    def ParseOrderParameters(operation, **inputParameters):
3579        """
3580        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3581
3582        :param operation: string "Buy" or "Sell".
3583        :param inputParameters: this is dict of strings that looks like this
3584               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3585               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3586               "prices" key: one or more prices to open limit-orders
3587               Counts of values in lots and prices lists must be equals!
3588        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3589        """
3590        # TODO: update order grid work with api v2
3591        pass
3592        # uLogger.debug("Input parameters: {}".format(inputParameters))
3593        #
3594        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3595        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3596        #     raise Exception("Incorrect value")
3597        #
3598        # if "l" in inputParameters.keys():
3599        #     inputParameters["lots"] = inputParameters.pop("l")
3600        #
3601        # if "p" in inputParameters.keys():
3602        #     inputParameters["prices"] = inputParameters.pop("p")
3603        #
3604        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3605        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3606        #     raise Exception("Incorrect value")
3607        #
3608        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3609        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3610        #
3611        # if len(lots) != len(prices):
3612        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3613        #     raise Exception("Incorrect value")
3614        #
3615        # uLogger.debug("Extracted parameters for orders:")
3616        # uLogger.debug("lots = {}".format(lots))
3617        # uLogger.debug("prices = {}".format(prices))
3618        #
3619        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3620        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3621        # uLogger.debug("Order parameters: {}".format(result))
3622        #
3623        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3625    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3626        """
3627        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3628
3629        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3630        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3631        """
3632        result = False
3633        msg = "Instrument not defined!"
3634
3635        if portfolio is None or not portfolio:
3636            portfolio = self.Overview(show=False)
3637
3638        if self._ticker:
3639            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3640            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3641
3642            for iType in TKS_INSTRUMENTS:
3643                for instrument in portfolio["stat"][iType]:
3644                    if instrument["ticker"] == self._ticker:
3645                        result = True
3646                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3647                        break
3648
3649        elif self._figi:
3650            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3651            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3652
3653            for iType in TKS_INSTRUMENTS:
3654                for instrument in portfolio["stat"][iType]:
3655                    if instrument["figi"] == self._figi:
3656                        result = True
3657                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3658                        break
3659
3660        else:
3661            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3662
3663        uLogger.debug(msg)
3664
3665        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3667    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3668        """
3669        Returns instrument from the user's portfolio if it presents there.
3670        Instrument must be defined by `ticker` (highly priority) or `figi`.
3671
3672        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3673        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3674        """
3675        result = None
3676        msg = "Instrument not defined!"
3677
3678        if portfolio is None or not portfolio:
3679            portfolio = self.Overview(show=False)
3680
3681        if self._ticker:
3682            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3683            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3684
3685            for iType in TKS_INSTRUMENTS:
3686                for instrument in portfolio["stat"][iType]:
3687                    if instrument["ticker"] == self._ticker:
3688                        result = instrument
3689                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3690                        break
3691
3692        elif self._figi:
3693            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3694            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3695
3696            for iType in TKS_INSTRUMENTS:
3697                for instrument in portfolio["stat"][iType]:
3698                    if instrument["figi"] == self._figi:
3699                        result = instrument
3700                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3701                        break
3702
3703        else:
3704            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3705
3706        uLogger.debug(msg)
3707
3708        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3710    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3711        """
3712        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3713
3714        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3715
3716        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3717        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3718        """
3719        result = False
3720        msg = "Instrument not defined!"
3721
3722        if portfolio is None or not portfolio:
3723            portfolio = self.Overview(show=False)
3724
3725        if self._ticker:
3726            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3727            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3728
3729            for instrument in portfolio["stat"]["orders"]:
3730                if instrument["ticker"] == self._ticker:
3731                    result = True
3732                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3733                    break
3734
3735        elif self._figi:
3736            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3737            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3738
3739            for instrument in portfolio["stat"]["orders"]:
3740                if instrument["figi"] == self._figi:
3741                    result = True
3742                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3743                    break
3744
3745        else:
3746            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3747
3748        uLogger.debug(msg)
3749
3750        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3752    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3753        """
3754        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3755        Instrument must be defined by `ticker` (highly priority) or `figi`.
3756
3757        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3758
3759        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3760        :return: list with `orderID`s of limit orders.
3761        """
3762        result = []
3763        msg = "Instrument not defined!"
3764
3765        if portfolio is None or not portfolio:
3766            portfolio = self.Overview(show=False)
3767
3768        if self._ticker:
3769            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3770            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3771
3772            for instrument in portfolio["stat"]["orders"]:
3773                if instrument["ticker"] == self._ticker:
3774                    result.append(instrument["orderID"])
3775
3776            if result:
3777                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3778
3779        elif self._figi:
3780            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3781            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3782
3783            for instrument in portfolio["stat"]["orders"]:
3784                if instrument["figi"] == self._figi:
3785                    result.append(instrument["orderID"])
3786
3787            if result:
3788                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3789
3790        else:
3791            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3792
3793        uLogger.debug(msg)
3794
3795        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3797    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3798        """
3799        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3800
3801        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3802
3803        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3804        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3805        """
3806        result = False
3807        msg = "Instrument not defined!"
3808
3809        if portfolio is None or not portfolio:
3810            portfolio = self.Overview(show=False)
3811
3812        if self._ticker:
3813            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3814            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3815
3816            for instrument in portfolio["stat"]["stopOrders"]:
3817                if instrument["ticker"] == self._ticker:
3818                    result = True
3819                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3820                    break
3821
3822        elif self._figi:
3823            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3824            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3825
3826            for instrument in portfolio["stat"]["stopOrders"]:
3827                if instrument["figi"] == self._figi:
3828                    result = True
3829                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3830                    break
3831
3832        else:
3833            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3834
3835        uLogger.debug(msg)
3836
3837        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3839    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3840        """
3841        Returns list with all `orderID`s of opened stop orders for the instrument.
3842        Instrument must be defined by `ticker` (highly priority) or `figi`.
3843
3844        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3845
3846        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3847        :return: list with `orderID`s of stop orders.
3848        """
3849        result = []
3850        msg = "Instrument not defined!"
3851
3852        if portfolio is None or not portfolio:
3853            portfolio = self.Overview(show=False)
3854
3855        if self._ticker:
3856            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3857            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3858
3859            for instrument in portfolio["stat"]["stopOrders"]:
3860                if instrument["ticker"] == self._ticker:
3861                    result.append(instrument["orderID"])
3862
3863            if result:
3864                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3865
3866        elif self._figi:
3867            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3868            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3869
3870            for instrument in portfolio["stat"]["stopOrders"]:
3871                if instrument["figi"] == self._figi:
3872                    result.append(instrument["orderID"])
3873
3874            if result:
3875                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3876
3877        else:
3878            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3879
3880        uLogger.debug(msg)
3881
3882        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3884    def RequestLimits(self) -> dict:
3885        """
3886        Method for obtaining the available funds for withdrawal for current `accountId`.
3887
3888        See also:
3889        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3890        - `OverviewLimits()` method
3891
3892        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3893                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3894                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3895                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3896        """
3897        if self.accountId is None or not self.accountId:
3898            uLogger.error("Variable `accountId` must be defined for using this method!")
3899            raise Exception("Account ID required")
3900
3901        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3902
3903        self.body = str({"accountId": self.accountId})
3904        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3905        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3906
3907        if self.moreDebug:
3908            uLogger.debug("Records about available funds for withdrawal successfully received")
3909
3910        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3912    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3913        """
3914        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3915
3916        See also: `RequestLimits()`.
3917
3918        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3919        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3920        :return: dict with raw parsed data from server and some calculated statistics about it.
3921        """
3922        if self.accountId is None or not self.accountId:
3923            uLogger.error("Variable `accountId` must be defined for using this method!")
3924            raise Exception("Account ID required")
3925
3926        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3927
3928        view = {
3929            "rawLimits": rawLimits,
3930            "limits": {  # parsed data for every currency:
3931                "money": {  # this is an array of portfolio currency positions
3932                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3933                },
3934                "blocked": {  # this is an array of blocked currency
3935                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3936                },
3937                "blockedGuarantee": {  # this is locked money under collateral for futures
3938                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3939                },
3940            },
3941        }
3942
3943        # --- Prepare text table with limits in human-readable format:
3944        if show or onlyFiles:
3945            info = [
3946                "# Withdrawal limits\n\n",
3947                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3948                "* **Account ID:** [{}]\n".format(self.accountId),
3949            ]
3950
3951            if view["limits"]["money"]:
3952                info.extend([
3953                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3954                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3955                ])
3956
3957            else:
3958                info.append("\nNo withdrawal limits\n")
3959
3960            for curr in view["limits"]["money"].keys():
3961                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3962                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3963                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3964
3965                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3966                    "[{}]".format(curr),
3967                    "{:.2f}".format(view["limits"]["money"][curr]),
3968                    "{:.2f}".format(availableMoney),
3969                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3970                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3971                )
3972
3973                if curr == "rub":
3974                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3975
3976                else:
3977                    info.append(infoStr)
3978
3979            infoText = "".join(info)
3980
3981            if show and not onlyFiles:
3982                uLogger.info(infoText)
3983
3984            if self.withdrawalLimitsFile and (show or onlyFiles):
3985                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3986                    fH.write(infoText)
3987
3988                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3989
3990                if self.useHTMLReports:
3991                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3992                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3993                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3994
3995                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3996
3997        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3999    def RequestAccounts(self) -> dict:
4000        """
4001        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4002
4003        See also:
4004        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4005        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4006        - `OverviewUserInfo()` method
4007
4008        :return: dict with raw data from server that contains accounts info. Example of dict:
4009                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4010                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4011                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4012                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4013        """
4014        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4015
4016        self.body = str({})
4017        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4018        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4019
4020        if self.moreDebug:
4021            uLogger.debug("Records about available accounts successfully received")
4022
4023        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4025    def RequestUserInfo(self) -> dict:
4026        """
4027        Method for requesting common user's information.
4028
4029        See also:
4030        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4031        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4032        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4033        - `OverviewUserInfo()` method
4034
4035        :return: dict with raw data from server that contains user's information. Example of dict:
4036                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4037                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4038        """
4039        uLogger.debug("Requesting common user's information. Wait, please...")
4040
4041        self.body = str({})
4042        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4043        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4044
4045        if self.moreDebug:
4046            uLogger.debug("Records about current user successfully received")
4047
4048        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4050    def RequestMarginStatus(self, accountId: str = None) -> dict:
4051        """
4052        Method for requesting margin calculation for defined account ID.
4053
4054        See also:
4055        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4056        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4057        - `OverviewUserInfo()` method
4058
4059        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4060        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4061                 Example of responses:
4062                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4063                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4064                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4065                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4066                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4067                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4068        """
4069        if accountId is None or not accountId:
4070            if self.accountId is None or not self.accountId:
4071                uLogger.error("Variable `accountId` must be defined for using this method!")
4072                raise Exception("Account ID required")
4073
4074            else:
4075                accountId = self.accountId  # use `self.accountId` (main ID) by default
4076
4077        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4078
4079        self.body = str({"accountId": accountId})
4080        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4081        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4082
4083        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4084            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4085            rawMargin = {}
4086
4087        else:
4088            if self.moreDebug:
4089                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4090
4091        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4093    def RequestTariffLimits(self) -> dict:
4094        """
4095        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4096
4097        See also:
4098        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4099        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4100        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4101        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4102        - `OverviewUserInfo()` method
4103
4104        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4105                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4106                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4107        """
4108        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4109
4110        self.body = str({})
4111        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4112        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4113
4114        if self.moreDebug:
4115            uLogger.debug("Records with limits of current tariff successfully received")
4116
4117        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4119    def RequestBondCoupons(self, iJSON: dict) -> dict:
4120        """
4121        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4122        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4123        All dates are in UTC timezone.
4124
4125        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4126        Documentation:
4127        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4128        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4129
4130        See also: `ExtendBondsData()`.
4131
4132        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4133                      If raw iJSON is not data of bond then server returns an error [400] with message:
4134                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4135        :return: dictionary with bond payment calendar. Response example
4136                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4137                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4138                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4139                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4140        """
4141        if iJSON["figi"] is None or not iJSON["figi"]:
4142            uLogger.error("FIGI must be defined for using this method!")
4143            raise Exception("FIGI required")
4144
4145        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4146        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4147
4148        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4149            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4150            self._figi,
4151            startDate,
4152            endDate,
4153        ))
4154
4155        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4156        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4157        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4158
4159        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4160            uLogger.warning("Instrument type is not bond!")
4161
4162        else:
4163            if self.moreDebug:
4164                uLogger.debug("Records about bond payment calendar successfully received")
4165
4166        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4168    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4169        """
4170        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4171        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4172        coupon yields, current yields and some statistics etc.
4173
4174        WARNING! This is too long operation if a lot of bonds requested from broker server.
4175
4176        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4177
4178        :param instruments: list of strings with tickers or FIGIs.
4179        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4180                     for further used by data scientists or stock analytics.
4181        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4182                 In XLSX-file and Pandas DataFrame fields mean:
4183                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4184                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4185        """
4186        if instruments is None or not instruments:
4187            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4188            raise Exception("Ticker or FIGI required")
4189
4190        if isinstance(instruments, str):
4191            instruments = [instruments]
4192
4193        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4194
4195        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4196
4197        iCount = len(uniqueInstruments)
4198        tooLong = iCount >= 20
4199        if tooLong:
4200            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4201
4202        bonds = None
4203        for i, self._figi in enumerate(uniqueInstruments):
4204            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4205
4206            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4207                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4208                rawBond = self.SearchByFIGI(requestPrice=True)
4209
4210                # Widen raw data with UTC current time (iData["actualDateTime"]):
4211                actualDate = datetime.now(tzutc())
4212                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4213
4214                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4215                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4216
4217                # Replace some values with human-readable:
4218                iData["nominalCurrency"] = iData["nominal"]["currency"]
4219                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4220                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4221                iData["aciCurrency"] = iData["aciValue"]["currency"]
4222                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4223                iData["issueSize"] = int(iData["issueSize"])
4224                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4225                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4226                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4227                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4228                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4229                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4230                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4231                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4232                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4233                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4234
4235                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4236                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4237                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4238                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4239                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4240                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4241                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4242                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4243                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4244                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4245                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4246
4247                # Widen raw data with calendar data from `rawCalendar` values:
4248                calendarData = []
4249                if "events" in iData["rawCalendar"].keys():
4250                    for item in iData["rawCalendar"]["events"]:
4251                        calendarData.append({
4252                            "couponDate": item["couponDate"],
4253                            "couponNumber": int(item["couponNumber"]),
4254                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4255                            "payCurrency": item["payOneBond"]["currency"],
4256                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4257                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4258                            "couponStartDate": item["couponStartDate"],
4259                            "couponEndDate": item["couponEndDate"],
4260                            "couponPeriod": item["couponPeriod"],
4261                        })
4262
4263                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4264                    if "maturityDate" not in iData.keys():
4265                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4266
4267                # Widen raw data with Coupon Rate.
4268                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4269                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4270                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4271                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4272
4273                # Widen raw data with Yield to Maturity (YTM) on current date.
4274                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4275                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4276                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4277                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4278                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4279                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4280
4281                iData["calendar"] = calendarData  # adds calendar at the end
4282
4283                # Remove not used data:
4284                iData.pop("uid")
4285                iData.pop("positionUid")
4286                iData.pop("currentPrice")
4287                iData.pop("rawCalendar")
4288
4289                colNames = list(iData.keys())
4290                if bonds is None:
4291                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4292
4293                else:
4294                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4295
4296            else:
4297                uLogger.warning("Instrument is not a bond!")
4298
4299            processed = round(100 * (i + 1) / iCount, 1)
4300            if tooLong and processed % 5 == 0:
4301                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4302
4303            else:
4304                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4305
4306        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4307
4308        # Saving bonds from Pandas DataFrame to XLSX sheet:
4309        if xlsx and self.bondsXLSXFile:
4310            with pd.ExcelWriter(
4311                    path=self.bondsXLSXFile,
4312                    date_format=TKS_DATE_FORMAT,
4313                    datetime_format=TKS_DATE_TIME_FORMAT,
4314                    mode="w",
4315            ) as writer:
4316                bonds.to_excel(
4317                    writer,
4318                    sheet_name="Extended bonds data",
4319                    index=True,
4320                    encoding="UTF-8",
4321                    freeze_panes=(1, 1),
4322                )  # saving as XLSX-file with freeze first row and column as headers
4323
4324            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4325
4326        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4328    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4329        """
4330        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4331
4332        WARNING! This is too long operation if a lot of bonds requested from broker server.
4333
4334        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4335
4336        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4337                        extended information about bonds: main info, current prices, bond payment calendar,
4338                        coupon yields, current yields and some statistics etc.
4339                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4340        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4341                     for further used by data scientists or stock analytics.
4342        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4343        """
4344        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4345            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4346
4347        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4348
4349        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4350        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4351        calendar = None
4352        for bond in extBonds.iterrows():
4353            for item in bond[1]["calendar"]:
4354                cData = {
4355                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4356                    "couponDate": item["couponDate"],
4357                    "figi": bond[1]["figi"],
4358                    "ticker": bond[1]["ticker"],
4359                    "name": bond[1]["name"],
4360                    "couponNumber": item["couponNumber"],
4361                    "payOneBond": item["payOneBond"],
4362                    "payCurrency": item["payCurrency"],
4363                    "couponType": item["couponType"],
4364                    "couponPeriod": item["couponPeriod"],
4365                    "fixDate": item["fixDate"],
4366                    "couponStartDate": item["couponStartDate"],
4367                    "couponEndDate": item["couponEndDate"],
4368                }
4369
4370                if calendar is None:
4371                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4372
4373                else:
4374                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4375
4376        if calendar is not None:
4377            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4378
4379            # Saving calendar from Pandas DataFrame to XLSX sheet:
4380            if xlsx:
4381                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4382
4383                with pd.ExcelWriter(
4384                        path=xlsxCalendarFile,
4385                        date_format=TKS_DATE_FORMAT,
4386                        datetime_format=TKS_DATE_TIME_FORMAT,
4387                        mode="w",
4388                ) as writer:
4389                    humanReadable = calendar.copy(deep=True)
4390                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4391                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4392                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4393                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4394                    humanReadable.columns = colNames  # human-readable column names
4395
4396                    humanReadable.to_excel(
4397                        writer,
4398                        sheet_name="Bond payments calendar",
4399                        index=False,
4400                        encoding="UTF-8",
4401                        freeze_panes=(1, 2),
4402                    )  # saving as XLSX-file with freeze first row and column as headers
4403
4404                    del humanReadable  # release df in memory
4405
4406                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4407
4408        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, show: bool = True, onlyFiles=False) -> str:
4410    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4411        """
4412        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4413        Also, creates Markdown file with calendar data, `calendar.md` by default.
4414
4415        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4416
4417        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4418                        extended information about bonds: main info, current prices, bond payment calendar,
4419                        coupon yields, current yields and some statistics etc.
4420                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4421        :param show: if `True` then also printing bonds payment calendar to the console,
4422                     otherwise save to file `calendarFile` only. `False` by default.
4423        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4424        :return: multilines text in Markdown format with bonds payment calendar as a table.
4425        """
4426        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4427            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4428
4429        infoText = "# Bond payments calendar\n\n"
4430
4431        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4432
4433        if not (calendar is None or calendar.empty):
4434            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4435
4436            info = [
4437                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4438                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4439                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4440            ]
4441
4442            newMonth = False
4443            notOneBond = calendar["figi"].nunique() > 1
4444            for i, bond in enumerate(calendar.iterrows()):
4445                if newMonth and notOneBond:
4446                    info.append(splitLine)
4447
4448                info.append(
4449                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4450                        "  √" if bond[1]["paid"] else "  —",
4451                        bond[1]["couponDate"].split("T")[0],
4452                        bond[1]["figi"],
4453                        bond[1]["ticker"],
4454                        bond[1]["couponNumber"],
4455                        "{} {}".format(
4456                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4457                            bond[1]["payCurrency"],
4458                        ),
4459                        bond[1]["couponType"],
4460                        bond[1]["couponPeriod"],
4461                        bond[1]["fixDate"].split("T")[0],
4462                    )
4463                )
4464
4465                if i < len(calendar.values) - 1:
4466                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4467                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4468                    newMonth = False if curDate.month == nextDate.month else True
4469
4470                else:
4471                    newMonth = False
4472
4473            infoText += "".join(info)
4474
4475            if show and not onlyFiles:
4476                uLogger.info("{}".format(infoText))
4477
4478            if self.calendarFile is not None and (show or onlyFiles):
4479                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4480                    fH.write(infoText)
4481
4482                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4483
4484                if self.useHTMLReports:
4485                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4486                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4487                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4488
4489                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4490
4491        else:
4492            infoText += "No data\n"
4493
4494        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4496    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4497        """
4498        Method for parsing and show simple table with all available user accounts.
4499
4500        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4501
4502        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4503        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4504        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4505                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4506                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4507                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4508                                                        "closed": "—", "access": "Full access" }, ...}}`
4509        """
4510        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4511
4512        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4513        accounts = {
4514            item["id"]: {
4515                "type": TKS_ACCOUNT_TYPES[item["type"]],
4516                "name": item["name"],
4517                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4518                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4519                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4520                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4521            } for item in rawAccounts["accounts"]
4522        }
4523
4524        # Raw and parsed data with some fields replaced in "stat" section:
4525        view = {
4526            "rawAccounts": rawAccounts,
4527            "stat": accounts,
4528        }
4529
4530        # --- Prepare simple text table with only accounts data in human-readable format:
4531        if show or onlyFiles:
4532            info = [
4533                "# User accounts\n\n",
4534                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4535                "| Account ID   | Type                      | Status                    | Name                           |\n",
4536                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4537            ]
4538
4539            for account in view["stat"].keys():
4540                info.extend([
4541                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4542                        account,
4543                        view["stat"][account]["type"],
4544                        view["stat"][account]["status"],
4545                        view["stat"][account]["name"],
4546                    )
4547                ])
4548
4549            infoText = "".join(info)
4550
4551            if show and not onlyFiles:
4552                uLogger.info(infoText)
4553
4554            if self.userAccountsFile and (show or onlyFiles):
4555                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4556                    fH.write(infoText)
4557
4558                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4559
4560                if self.useHTMLReports:
4561                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4562                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4563                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4564
4565                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4566
4567        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4569    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4570        """
4571        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4572
4573        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4574
4575        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4576        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4577        :return: dict with raw parsed data from server and some calculated statistics about it.
4578        """
4579        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4580        tmpTicker = self._ticker
4581        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4582        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4583        self._ticker = tmpTicker
4584
4585        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4586        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4587        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4588        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4589        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4590        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4591
4592        # This is dict with parsed common user data:
4593        userInfo = {
4594            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4595            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4596            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4597            "tariff": rawUserInfo["tariff"],
4598        }
4599
4600        # This is an array of dict with parsed margin statuses for every account IDs:
4601        margins = {}
4602        for accountId in accounts.keys():
4603            if rawMargins[accountId]:
4604                margins[accountId] = {
4605                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4606                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4607                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4608                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4609                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4610                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4611                    "missing": missing["volume"],
4612                }
4613
4614            else:
4615                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4616
4617        unary = {}  # unary-connection limits
4618        for item in rawTariffLimits["unaryLimits"]:
4619            if item["limitPerMinute"] in unary.keys():
4620                unary[item["limitPerMinute"]].extend(item["methods"])
4621
4622            else:
4623                unary[item["limitPerMinute"]] = item["methods"]
4624
4625        stream = {}  # stream-connection limits
4626        for item in rawTariffLimits["streamLimits"]:
4627            if item["limit"] in stream.keys():
4628                stream[item["limit"]].extend(item["streams"])
4629
4630            else:
4631                stream[item["limit"]] = item["streams"]
4632
4633        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4634        limits = {
4635            "unary": unary,
4636            "stream": stream,
4637        }
4638
4639        # Raw and parsed data as an output result:
4640        view = {
4641            "rawUserInfo": rawUserInfo,
4642            "rawAccounts": rawAccounts,
4643            "rawMargins": rawMargins,
4644            "rawTariffLimits": rawTariffLimits,
4645            "stat": {
4646                "overview": overview,
4647                "userInfo": userInfo,
4648                "accounts": accounts,
4649                "margins": margins,
4650                "limits": limits,
4651            },
4652        }
4653
4654        # --- Prepare text table with user information in human-readable format:
4655        if show or onlyFiles:
4656            info = [
4657                "# Full user information\n\n",
4658                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4659                "## Common information\n\n",
4660                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4661                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4662                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4663                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4664                "\n## User accounts\n\n",
4665            ]
4666
4667            for account in view["stat"]["accounts"].keys():
4668                info.extend([
4669                    "### ID: [{}]\n\n".format(account),
4670                    "| Parameters           | Values                                                       |\n",
4671                    "|----------------------|--------------------------------------------------------------|\n",
4672                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4673                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4674                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4675                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4676                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4677                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4678                ])
4679
4680                if margins[account]:
4681                    info.extend([
4682                        "| Margin status:       | Enabled                                                      |\n",
4683                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4684                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4685                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4686                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4687                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4688                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4689                    ])
4690
4691                else:
4692                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4693
4694            info.extend([
4695                "\n## Current user tariff limits\n",
4696                "\n### See also\n",
4697                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4698                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4699                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4700                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4701                "\n### Unary limits\n",
4702            ])
4703
4704            if unary:
4705                for key, values in sorted(unary.items()):
4706                    info.append("\n* Max requests per minute: {}\n".format(key))
4707
4708                    for value in values:
4709                        info.append("  - {}\n".format(value))
4710
4711            else:
4712                info.append("\nNot available\n")
4713
4714            info.append("\n### Stream limits\n")
4715
4716            if stream:
4717                for key, values in sorted(stream.items()):
4718                    info.append("\n* Max stream connections: {}\n".format(key))
4719
4720                    for value in values:
4721                        info.append("  - {}\n".format(value))
4722
4723            else:
4724                info.append("\nNot available\n")
4725
4726            infoText = "".join(info)
4727
4728            if show and not onlyFiles:
4729                uLogger.info(infoText)
4730
4731            if self.userInfoFile and (show or onlyFiles):
4732                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4733                    fH.write(infoText)
4734
4735                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4736
4737                if self.useHTMLReports:
4738                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4739                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4740                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4741
4742                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4743
4744        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4747class Args:
4748    """
4749    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4750    """
4751    def __init__(self, **kwargs):
4752        self.__dict__.update(kwargs)
4753
4754    def __getattr__(self, item):
4755        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4751    def __init__(self, **kwargs):
4752        self.__dict__.update(kwargs)
def ParseArgs():
4758def ParseArgs():
4759    """This function get and parse command line keys."""
4760    parser = ArgumentParser()  # command-line string parser
4761
4762    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4763    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4764
4765    # --- options:
4766
4767    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4768    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4769    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4770
4771    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4772    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4773
4774    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4775    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4776
4777    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4778    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4779
4780    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4781    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4782    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4783
4784    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4785    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4786
4787    # --- commands:
4788
4789    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4790
4791    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4792    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4793    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4794    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4795    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4796    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4797    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4798    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4799
4800    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4801    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4802    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4803    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4804    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4805    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4806
4807    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4808    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4809    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4810    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4811
4812    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4813    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4814    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4815
4816    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4817    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4818    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4819    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4820    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4821    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4822    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4823
4824    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4825    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4826    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4827    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4828    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4829
4830    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4831    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4832    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4833
4834    cmdArgs = parser.parse_args()
4835    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4838def Main(**kwargs):
4839    """
4840    Main function for work with TKSBrokerAPI in the console.
4841
4842    See examples:
4843    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4844    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4845    """
4846    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4847
4848    if args.debug_level:
4849        uLogger.level = 10  # always debug level by default
4850        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4851
4852    exitCode = 0
4853    start = datetime.now(tzutc())
4854    uLogger.debug("=-" * 50)
4855    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4856        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4857        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4858    ))
4859
4860    # trying to calculate full current version:
4861    buildVersion = __version__
4862    try:
4863        v = version("tksbrokerapi")
4864        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4865
4866    except Exception:
4867        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4868
4869    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4870    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4871
4872    try:
4873        if args.version:
4874            print("TKSBrokerAPI {}".format(buildVersion))
4875            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4876
4877        else:
4878            # Init class for trading with Tinkoff Broker:
4879            trader = TinkoffBrokerServer(
4880                token=args.token,
4881                accountId=args.account_id,
4882                useCache=not args.no_cache,
4883            )
4884
4885            # --- set some options:
4886
4887            if args.more:
4888                trader.moreDebug = True
4889                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4890
4891            if args.html:
4892                trader.useHTMLReports = True
4893
4894            if args.ticker:
4895                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4896
4897                if ticker in trader.aliasesKeys:
4898                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4899
4900                else:
4901                    trader.ticker = ticker
4902
4903            if args.figi:
4904                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4905
4906            if args.depth is not None:
4907                trader.depth = args.depth
4908
4909            # --- do one command:
4910
4911            if args.list:
4912                if args.output is not None:
4913                    trader.instrumentsFile = args.output
4914
4915                trader.ShowInstrumentsInfo(show=True)
4916
4917            elif args.list_xlsx:
4918                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4919
4920            elif args.bonds_xlsx is not None:
4921                if args.output is not None:
4922                    trader.bondsXLSXFile = args.output
4923
4924                if len(args.bonds_xlsx) == 0:
4925                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4926
4927                else:
4928                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4929
4930            elif args.search:
4931                if args.output is not None:
4932                    trader.searchResultsFile = args.output
4933
4934                trader.SearchInstruments(pattern=args.search[0], show=True)
4935
4936            elif args.info:
4937                if not (args.ticker or args.figi):
4938                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4939                    raise Exception("Ticker or FIGI required")
4940
4941                if args.output is not None:
4942                    trader.infoFile = args.output
4943
4944                if args.ticker:
4945                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4946
4947                else:
4948                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4949
4950            elif args.calendar is not None:
4951                if args.output is not None:
4952                    trader.calendarFile = args.output
4953
4954                if len(args.calendar) == 0:
4955                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4956
4957                else:
4958                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4959
4960                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4961
4962            elif args.price:
4963                if not (args.ticker or args.figi):
4964                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4965                    raise Exception("Ticker or FIGI required")
4966
4967                trader.GetCurrentPrices(show=True)
4968
4969            elif args.prices is not None:
4970                if args.output is not None:
4971                    trader.pricesFile = args.output
4972
4973                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4974
4975            elif args.overview:
4976                if args.output is not None:
4977                    trader.overviewFile = args.output
4978
4979                trader.Overview(show=True, details="full")
4980
4981            elif args.overview_digest:
4982                if args.output is not None:
4983                    trader.overviewDigestFile = args.output
4984
4985                trader.Overview(show=True, details="digest")
4986
4987            elif args.overview_positions:
4988                if args.output is not None:
4989                    trader.overviewPositionsFile = args.output
4990
4991                trader.Overview(show=True, details="positions")
4992
4993            elif args.overview_orders:
4994                if args.output is not None:
4995                    trader.overviewOrdersFile = args.output
4996
4997                trader.Overview(show=True, details="orders")
4998
4999            elif args.overview_analytics:
5000                if args.output is not None:
5001                    trader.overviewAnalyticsFile = args.output
5002
5003                trader.Overview(show=True, details="analytics")
5004
5005            elif args.overview_calendar:
5006                if args.output is not None:
5007                    trader.overviewAnalyticsFile = args.output
5008
5009                trader.Overview(show=True, details="calendar")
5010
5011            elif args.deals is not None:
5012                if args.output is not None:
5013                    trader.reportFile = args.output
5014
5015                if 0 <= len(args.deals) < 3:
5016                    trader.Deals(
5017                        start=args.deals[0] if len(args.deals) >= 1 else None,
5018                        end=args.deals[1] if len(args.deals) == 2 else None,
5019                        show=True,  # Always show deals report in console
5020                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5021                    )
5022
5023                else:
5024                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5025                    raise Exception("Incorrect value")
5026
5027            elif args.history is not None:
5028                if args.output is not None:
5029                    trader.historyFile = args.output
5030
5031                if 0 <= len(args.history) < 3:
5032                    dataReceived = trader.History(
5033                        start=args.history[0] if len(args.history) >= 1 else None,
5034                        end=args.history[1] if len(args.history) == 2 else None,
5035                        interval="hour" if args.interval is None or not args.interval else args.interval,
5036                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5037                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5038                        show=True,  # shows all downloaded candles in console
5039                    )
5040
5041                    if args.render_chart is not None and dataReceived is not None:
5042                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5043
5044                        trader.ShowHistoryChart(
5045                            candles=dataReceived,
5046                            interact=iChart,
5047                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5048                        )
5049
5050                else:
5051                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5052                    raise Exception("Incorrect value")
5053
5054            elif args.load_history is not None:
5055                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5056
5057                if args.render_chart is not None and histData is not None:
5058                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5059                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5060
5061                    trader.ShowHistoryChart(
5062                        candles=histData,
5063                        interact=iChart,
5064                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5065                    )
5066
5067            elif args.trade is not None:
5068                if 1 <= len(args.trade) <= 5:
5069                    trader.Trade(
5070                        operation=args.trade[0],
5071                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5072                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5073                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5074                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5075                    )
5076
5077                else:
5078                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5079
5080            elif args.buy is not None:
5081                if 0 <= len(args.buy) <= 4:
5082                    trader.Buy(
5083                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5084                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5085                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5086                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5087                    )
5088
5089                else:
5090                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5091
5092            elif args.sell is not None:
5093                if 0 <= len(args.sell) <= 4:
5094                    trader.Sell(
5095                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5096                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5097                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5098                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5099                    )
5100
5101                else:
5102                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5103
5104            elif args.order:
5105                if 4 <= len(args.order) <= 7:
5106                    trader.Order(
5107                        operation=args.order[0],
5108                        orderType=args.order[1],
5109                        lots=int(args.order[2]),
5110                        targetPrice=float(args.order[3]),
5111                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5112                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5113                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5114                    )
5115
5116                else:
5117                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5118
5119            elif args.buy_limit:
5120                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5121
5122            elif args.sell_limit:
5123                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5124
5125            elif args.buy_stop:
5126                if 2 <= len(args.buy_stop) <= 7:
5127                    trader.BuyStop(
5128                        lots=int(args.buy_stop[0]),
5129                        targetPrice=float(args.buy_stop[1]),
5130                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5131                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5132                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5133                    )
5134
5135                else:
5136                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5137
5138            elif args.sell_stop:
5139                if 2 <= len(args.sell_stop) <= 7:
5140                    trader.SellStop(
5141                        lots=int(args.sell_stop[0]),
5142                        targetPrice=float(args.sell_stop[1]),
5143                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5144                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5145                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5146                    )
5147
5148                else:
5149                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5150
5151            # elif args.buy_order_grid is not None:
5152            #     # update order grid work with api v2
5153            #     if len(args.buy_order_grid) == 2:
5154            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5155            #
5156            #         for order in orderParams:
5157            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5158            #
5159            #     else:
5160            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5161            #
5162            # elif args.sell_order_grid is not None:
5163            #     # update order grid work with api v2
5164            #     if len(args.sell_order_grid) >= 2:
5165            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5166            #
5167            #         for order in orderParams:
5168            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5169            #
5170            #     else:
5171            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5172
5173            elif args.close_order is not None:
5174                trader.CloseOrders(args.close_order)  # close only one order
5175
5176            elif args.close_orders is not None:
5177                trader.CloseOrders(args.close_orders)  # close list of orders
5178
5179            elif args.close_trade:
5180                if not (args.ticker or args.figi):
5181                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5182                    raise Exception("Ticker or FIGI required")
5183
5184                if args.ticker:
5185                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5186
5187                else:
5188                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5189
5190            elif args.close_trades is not None:
5191                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5192
5193            elif args.close_all is not None:
5194                if args.ticker:
5195                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5196
5197                elif args.figi:
5198                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5199
5200                else:
5201                    trader.CloseAll(*args.close_all)
5202
5203            elif args.limits:
5204                if args.output is not None:
5205                    trader.withdrawalLimitsFile = args.output
5206
5207                trader.OverviewLimits(show=True)
5208
5209            elif args.user_info:
5210                if args.output is not None:
5211                    trader.userInfoFile = args.output
5212
5213                trader.OverviewUserInfo(show=True)
5214
5215            elif args.account:
5216                if args.output is not None:
5217                    trader.userAccountsFile = args.output
5218
5219                trader.OverviewAccounts(show=True)
5220
5221            else:
5222                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5223                raise Exception("There is no command to execute")
5224
5225    except Exception:
5226        trace = tb.format_exc()
5227        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5228            if e in trace:
5229                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5230                break
5231
5232        uLogger.debug(trace)
5233        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5234        exitCode = 255  # an error occurred, must be open a ticket for this issue
5235
5236    finally:
5237        finish = datetime.now(tzutc())
5238
5239        if exitCode == 0:
5240            if args.more:
5241                uLogger.debug("All operations were finished success (summary code is 0).")
5242
5243        else:
5244            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5245                os.path.abspath(uLog.defaultLogFile), exitCode,
5246            ))
5247
5248        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5249        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5250            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5251            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5252        ))
5253        uLogger.debug("=-" * 50)
5254
5255        if not kwargs:
5256            sys.exit(exitCode)
5257
5258        else:
5259            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: